存储库方法 vs 扩展IQueryable

33
我有一些仓库 (例如: ContactRepository, UserRepository等) ,它们封装了对领域模型的数据访问。
当我在寻找数据时,例如:
- 查找一个以XYZ开头的联系人 - 查找一个生日在1960年之后的联系人 - 等等
我开始实现仓库方法,例如 `FirstNameStartsWith(string prefix)` 和 `YoungerThanBirthYear(int year)`,基本上是按照众多示例的做法。
然后我遇到了一个问题 - 如果我必须结合多个搜索条件怎么办?我的每个仓库搜索方法(如上所述)只返回一个有限的实际领域对象集。为了寻求更好的方法,我开始在IQueryable<T>上编写扩展方法,例如:
public static IQueryable<Contact> FirstNameStartsWith(
               this IQueryable<Contact> contacts, String prefix)
{
    return contacts.Where(
        contact => contact.FirstName.StartsWith(prefix));
}        

现在我可以做的事情包括:

ContactRepository.GetAll().FirstNameStartsWith("tex").YoungerThanBirthYear(1960);

然而,我发现自己在各处编写扩展方法(并发明了像 ContactsQueryableExtensions 这样的疯狂类),失去了将所有内容放在适当仓库中的“良好分组”。

这是真正的做法吗?还是有更好的方法来实现相同的目标?

4个回答

12

我最近在思考这个问题,因为我刚开始在目前的工作中。我习惯于使用Repositories,它们沿着完整的IQueryable路径进行,只使用了您建议的基本的Repositories。

我觉得仓储模式很好,可以在一定程度上描述您想要如何处理应用程序域中的数据。但是,您所描述的问题确实存在。除了简单的应用程序外,它会变得混乱而快速。

也许有重新考虑为什么要以这么多方式请求数据的方法吗?如果没有,我真的认为混合方法是最好的选择。为您重复使用的东西创建仓库方法。对于真正有意义的事情。DRY和所有其他的。但是那些只出现一次的呢?为什么不充分利用IQueryable和你可以做的性感之事呢?就像你说的那样,创建一个方法很傻,但这并不意味着您不需要数据。DRY在那里并不适用,不是吗?

这需要纪律才能做到做得好,但是我真的认为这是一条适当的路线。


8

@Alex - 我知道这是一个老问题,但我要做的就是让仓储层只处理非常简单的东西。这意味着,获取表或视图的所有记录。

然后,在服务层(您正在使用分层解决方案,对吧?:))中,我将处理所有“特殊”的查询内容。

好的,现在来举个例子。

仓储层

ContactRepository.cs

public IQueryable<Contact> GetContacts()
{
    return (from q in SqlContext.Contacts
            select q).AsQueryable();
}

很简单明了。 SqlContext 是您的 EF Context 实例.. 它上面有一个名为 ContactsEntity .. 这基本上是您的 sql Contacts 类。
这意味着,该方法基本上在执行: SELECT * FROM CONTACTS ... 但它现在并没有实际访问数据库。
好的.. 下一层.. KICK ... 我们继续往上走(盗梦空间 中的任何人都可以吗?)
ContactService.cs

public  ICollection<Contact> FindContacts(string name)
{
    return FindContacts(name, null)
}

public ICollection<Contact> FindContacts(string name, int? year)
{
   IQueryable<Contact> query = _contactRepository.GetContacts();
   
   if (!string.IsNullOrEmpty(name))
   {
       query = from q in query
               where q.FirstName.StartsWith(name)
               select q;
   }

   if (int.HasValue)
   {
       query = from q in query
               where q.Birthday.Year <= year.Value
               select q);
    }

    return (from q in query
            select q).ToList();
}

完成。

让我们回顾一下。首先,我们从一个简单的“从联系人获取所有内容”的查询开始。现在,如果提供了一个姓名,请添加一个过滤器来按姓名过滤所有联系人。接下来,如果提供了年份,则按年份筛选生日。最后,我们使用修改后的查询命中数据库,并查看返回的结果。

注意:

  • 为简单起见,我省略了任何依赖注入。但强烈建议使用。
  • 这都是伪代码。未经测试(针对编译器),但你可以理解……

要点

  • 服务层处理所有智能操作。在那里,你决定需要什么数据。
  • 存储库是一个简单的SELECT * FROM TABLE或一个简单的INSERT/UPDATE到TABLE。

祝好运:)


1
我认为值得一提的是,这只有在使用支持延迟执行的DAL(例如Linq To Sql)时才能高效工作。如果我错了,请纠正我。否则,您将从数据存储中检索大量可能不会被使用的数据。现在,如果您知道用户将以多种不同方式使用相同的数据集,那么最终可能没问题,但如果他们只运行此查询一次,然后检索完全不相关的数据,则此设计将导致值得一提的开销。 - joshlrogers
1
我们都用过这种方法。对于每个额外的过滤器,我们都添加一个“可选参数”。我认为作者原始的示例更简洁、易于使用(和重复使用)。 - Jerod Houghtelling
1
@dude - 大错特错!你知道IQueryable是什么吗?它不会访问数据库。它是一个查询。所以我并没有真正执行SELECT * FROM XXX。我通过添加额外的where子句来扩展该查询...在需要时。因此,最终的SQL实际上是SELECT * FROM XXX WHERE blah(如果用户提供了可选的名称和/或可选的年份参数)。所以,除非我误解了你的意思...小心你说什么:( - Pure.Krome
1
@Jerod Houghtelling:黑客?这怎么算是黑客行为呢?根据作者的原始示例,他们将拥有一个非常复杂的存储库(正如他/她所建议的那样)...然后在服务层中进行了部分复制。如果你问我,这并不是很DRY。想要更详细的答案示例吗?看看这篇好博客文章:http://huyrua.wordpress.com/2010/07/13/entity-framework-4-poco-repository-and-specification-pattern/ - Pure.Krome
1
现在我明白了 :) 是的 - 我同意。但是我没有担心这个,因为这是一个 EF 的问题 :) - Pure.Krome
显示剩余3条评论

5
我知道这篇文章有点老了,但最近我也遇到了同样的问题,和Chad一样,我得出了一个结论:只要稍加练习,扩展方法和仓储方法的混合使用似乎是最好的选择。
以下是我在(Entity Framework)应用程序中遵循的一些通用规则:

查询排序

如果该方法仅用于排序,我更喜欢编写操作IQueryable<T>IOrderedQueryable<T>的扩展方法(以利用底层提供程序)。例如:
public static IOrderedQueryable<TermRegistration> ThenByStudentName(
    this IOrderedQueryable<TermRegistration> query)
{
    return query
        .ThenBy(reg => reg.Student.FamilyName)
        .ThenBy(reg => reg.Student.GivenName);
}

现在我可以在我的存储库类中根据需要使用ThenByStudentName()

返回单个实例的查询

如果该方法涉及通过原始参数进行查询,则通常需要一个ObjectContext,并且不易于使其成为static。这些方法我会留在我的存储库中,例如:

public Student GetById(int id)
{
    // Calls context.ObjectSet<T>().SingleOrDefault(predicate) 
    // on my generic EntityRepository<T> class 
    return SingleOrDefault(student => student.Active && student.Id == id);
}

然而,如果该方法涉及使用其导航属性查询EntityObject,则通常可以很容易地将其实现为静态方法,并作为扩展方法实现。例如:

public static TermRegistration GetLatestRegistration(this Student student)
{
    return student.TermRegistrations.AsQueryable()
        .OrderByTerm()
        .FirstOrDefault();
}

现在我可以方便地编写someStudent.GetLatestRegistration(),而无需在当前作用域中使用存储库实例。

返回集合的查询

如果方法返回IEnumerableICollectionIList,我会尽可能将其设为static,即使它使用导航属性,也会留在存储库上。例如:
public static IList<TermRegistration> GetByTerm(Term term, bool ordered)
{
    var termReg = term.TermRegistrations;
    return (ordered)
        ? termReg.AsQueryable().OrderByStudentName().ToList()
        : termReg.ToList();
}

这是因为我的GetAll()方法已经存在于存储库中,这有助于避免扩展方法的混乱。另一个不实现这些“集合获取器”作为扩展方法的原因是它们需要更冗长的命名才能有意义,因为返回类型并不暗示。例如,最后一个示例将变成GetTermRegistrationsByTerm(this Term term)。希望这可以帮到你!

有意义的命名,也称为普遍语言,是服务层的一个关键目标和好处。GetRegistrationsByTerm() 是一个很好的例子。 - one.beat.consumer

0

六年后,我确定 @Alex 已经解决了他的问题,但在阅读接受的答案后,我想再加上我的一点建议。

IQueryable 集合扩展到 仓储库(repository) 中的通用目的是提供灵活性并赋予其消费者自定义数据检索的能力。Alex 已经做得很好了。

服务层(service layer) 的主要作用是遵守关注点分离(separation of concerns) 原则并处理与业务功能相关的命令逻辑。

在现实世界的应用程序中,查询逻辑(query logic) 通常不需要超出仓储本身提供的检索机制(例如,值更改、类型转换)的扩展。

考虑下面两种情况:

IQueryable<Vehicle> Vehicles { get; }

// raw data
public static IQueryable<Vehicle> OwnedBy(this IQueryable<Vehicle> query, int ownerId)
{
    return query.Where(v => v.OwnerId == ownerId);
}

// business purpose
public static IQueryable<Vehicle> UsedThisYear(this IQueryable<Vehicle> query)
{
    return query.Where(v => v.LastUsed.Year == DateTime.Now.Year);
}

这两种方法都是简单的查询扩展,但它们有不同的作用。第一种是一个简单的过滤器,而第二种则意味着业务需求(例如维护或计费)。在一个简单的应用程序中,你可以在存储库中实现它们。在一个更理想的系统中,UsedThisYear 最适合服务层(甚至可以实现为普通实例方法),在那里它也可以更好地促进 CQRS 策略,即分离命令和查询。

关键考虑因素是 (a) 你的存储库的主要目的和 (b) 你有多少愿意遵循 CQRS 和 DDD 哲学。


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