是否返回IQueryable<T>

74

我有一个仓库类,它包装了我的LINQ to SQL数据上下文。该仓库类是一个业务层类,包含所有的数据层逻辑(以及缓存等)。

这是我的Repo接口的第一版。

public interface ILocationRepository
{
    IList<Location> FindAll();
    IList<Location> FindForState(State state);
    IList<Location> FindForPostCode(string postCode);
}

为了处理FindAll的分页,我正在考虑是否应该公开IQueryable<ILocation>而不是IList,以简化像分页这样的情况下的接口。

公开数据存储库中的IQueryable有哪些优缺点?

非常感谢任何帮助。


8
在仓储接口中使用IEnumerable<T>怎么样? - Konstantin Tarkus
@KonstantinTarkus 我不赞成在 Repository 或 BLL 中使用 IEnumerable<T>,因为如果你没有注意并对同一个列表进行了多次 foreach 循环,它可能会影响性能,这种情况下你将多次调用 GetEnumerator() - Wahid Bitar
3个回答

90

优点; 可组合性:

  • 调用者可以添加过滤器
  • 调用者可以添加分页功能
  • 调用者可以添加排序功能
  • 等等

缺点; 非测试可性:

  • 你的存储库不再适合进行单元测试;你无法依赖于:a:它能够正常工作,b: 它做了什么
    • 调用者可能会添加一个不能转化为TSQL映射的函数(即没有TSQL映射,运行时出错)
    • 调用者可能会添加一个使其执行效率极低的过滤/排序功能
  • 因为调用者期望 IQueryable<T> 可以被组合,这排除了不可组合的实现 - 或者强制你为它们编写自己的查询提供程序
  • 这意味着你无法对 DAL 进行优化 / profiling

为了保持稳定性,我已经不再将 IQueryable<T>Expression<...> 暴露在我的存储库中。这意味着我知道存储库的行为,并且我的上层可以使用模拟对象而不必担心“实际的存储库是否支持这个?” (强制进行集成测试)。

我仍然在存储库内部使用 IQueryable<T> 等 - 但不是在边界上。我在这里发布了对此主题的更多思考。在仓储接口上添加分页参数同样容易。你甚至可以使用扩展方法(在接口上)添加可选的分页参数,以便具体类只需实现1个方法,但调用者可能有2或3个重载可用。


5
如果使用得当,IEnumerable<T> 可以是可以接受的——但主要用于大数据。对于常规查询,我更喜欢使用封闭集合(数组/列表等)。特别是我目前正在使用 MVC,并且希望 控制器 获取所有数据——而不是将其推迟到视图——否则无法完全测试... - Marc Gravell
2
...因为您实际上还没有证明controller获取了数据(因为IEnumerable<T>通常与延迟执行一起使用)。如果存储库返回IList<T>(或类似类型),那么您就知道已经获得了数据。 - Marc Gravell
2
使用IQueryable<T>,可以更改实际查询。使用IEnumerable<T>相对较少 - 但仍然:获取数据是控制器的责任,而不是视图。 - Marc Gravell
1
关于您最后提到的可选分页参数和调用者可用的2或3个重载,您能否指向一篇相关文章? - Shawn Mclean
1
虽然晚了3年,但我更喜欢尽可能地暴露IQueryable<T>。控制器可以进一步过滤它需要的内容,视图可以根据需要进一步过滤,甚至可以从控制器提供的内容中减少所需的字段。也许在视图中我并不需要Person类的每个字段。当我只想要他们的名字时,为什么要让存储库检索所有这些字段并将它们放入Person类中呢?至于测试,这取决于您要测试什么。您仍然可以测试存储库是否按请求返回数据。 - Robert McKee
显示剩余8条评论

7

如前面的答案所提到的,公开IQueryable会让调用者接触到IQueryable本身,这可能是危险的。

封装业务逻辑的首要责任是维护数据库的完整性。

您可以继续公开IList,并更改参数如下,这就是我们正在做的...

public interface ILocationRepository
{
    IList<Location> FindAll(int start, int size);
    IList<Location> FindForState(State state, int start, int size);
    IList<Location> FindForPostCode(string postCode, int start, int size);
}

如果 size == -1,则返回所有...

另一种方法...

如果您仍然想返回 IQueryable,则可以在函数内部返回 List 的 IQueryable.. 例如...

public class MyRepository
{
    IQueryable<Location> FindAll()
    {
        List<Location> myLocations = ....;
        return myLocations.AsQueryable<Location>;
        // here Query can only be applied on this
        // subset, not directly to the database
    }
}

第一种方法比内存更有优势,因为你只需返回少量数据而不是全部数据。

1
这并不太优雅。这种方式要求CVertex在创建每个存储库方法时都添加这些参数(start,size)- 这是非常糟糕的编程实践。 - twk
2
哎呀,你的第二种方法会导致应用程序崩溃!它会将所有结果都读入内存,然后执行LINQ查询。说实话,我从来没有找到使用“AsQueryable”的真正理由。我甚至不确定它为什么存在。它并不能突然将您的List转换为IQueryable,并允许您构建表达式树,以便将其发送到数据存储区,那么它的意义何在呢? - Chev
我同意,我很久以前写的时候对EF的工作原理知之甚少,我并不想将所有数据加载到内存中。 - Akash Kava
1
@AlexFord AsQueryable 在你从一个旧的 API 中获取到非泛型的 IEnumerable 时非常有用。 - joshperry
啊,我想这是有道理的。 - Chev
显示剩余2条评论

2

我建议使用 IEnumerable 替代 IList,这样你将拥有更大的灵活性。

这种方式可以让你只获取你真正需要的数据,而无需在存储库中进行额外的工作。

示例:

// Repository
public interface IRepository
{
    IEnumerable<Location> GetLocations();
}

// Controller
public ActionResult Locations(int? page)
{
    return View(repository.GetLocations().AsPagination(page ?? 1, 10);
}

这是非常干净简单的。


为什么?有哪些权衡? - Richard
1
如果您要向用户显示分页列表(假设从数据库中获取1000行,但只显示其中的10行),则IList<Location> FindAll()效率不高。使用IQueryable就不会有这样的问题。 - Konstantin Tarkus
顺便提一句,如果您要实现分页功能,请查看MvcContrib中现有的帮助类 = http://www.codeplex.com/mvccontrib/(MvcContrib.Pagination命名空间)。 - Konstantin Tarkus
我喜欢创建冗长的存储库方法,例如GetRecords(int page, int itemsPerPage),而不是将分页传递给控制器。 - Chev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接