处理具有多个具体实现的存储库模式中的查询?

发布于 2024-12-11 22:19:54 字数 1349 浏览 0 评论 0 原文

这更多的是一种学术好奇心,但我试图找出如何最好地完成以下任务。

想象一下这样一种情况,您有一个 Person 对象

public class Person {
    public string Name {get;set;}
    public int Age {get;set;}
}

和一个用于从某个持久性存储中检索它们的存储库合约...

public class IPersonRepository {
    public IEnumerable<Person> Search(*** SOME_METHOD_SIGNATURE ***);
}

您的消费者应用程序实际上并不关心具体的实现。它只是从 Unity/Ninject & 获取正确的具体实现。开始查询。

IPersonRespository repo = GetConcreteImplementationFromConfig();
repo.Search( ... );

我想知道你会使用什么作为你的方法签名,它既灵活又可靠。无论实现如何,都可以扩展。

选项 1。

public IEnumerable<Person> Search(Expression<Func<Person, bool>> expression);

这很好,因为如果您使用支持 LINQ 的数据上下文(例如 EntityFramework),则可以将表达式直接传递到您的上下文。如果您的实现必须使用手工制作的存储 procs/sql/ADO.NET 等,则此选项似乎会失败...

选项 2。

public IEnumerable<Person> Search(PersonSearch parameters);

public class PersonSearch {
    public int? Age {get;set;}
    public string FullName {get;set;}
    public string PartialName { get; set; }
}

这似乎是最灵活的(从某种意义上说它可以与 Linq、Plain Old SQL 一起使用,

但它只是有点“编写自己的查询语言”的味道,因为您需要考虑消费者可能想要执行的每个可能的查询。 年龄>=18&&年龄<=65 &&名称 LIKE '%John%'

选项 3

还有其他选项吗?

This is more of an academic curiosity but I'm trying to figure out how best to accomplish the following.

Imagine a situation where you have an Person object

public class Person {
    public string Name {get;set;}
    public int Age {get;set;}
}

and a Repository Contract for Retrieving them from some persistence store...

public class IPersonRepository {
    public IEnumerable<Person> Search(*** SOME_METHOD_SIGNATURE ***);
}

Your consumer application really doesn't care about the specific implementation. It's just gonna grab the correct concrete Implementation from Unity/Ninject & start querying.

IPersonRespository repo = GetConcreteImplementationFromConfig();
repo.Search( ... );

What I'm wondering is what would you use for your Method Signature here that's both flexible & extensible regardless of the implementation.

Option 1.

public IEnumerable<Person> Search(Expression<Func<Person, bool>> expression);

This is nice because if you're using a LINQ Capable (e.g. EntityFramework) data context, you can just pass the expression directly to your context. This option seems to fall down though if you're implementation has to use hand crafted stored procs/sql/ADO.NET etc...

Option 2.

public IEnumerable<Person> Search(PersonSearch parameters);

public class PersonSearch {
    public int? Age {get;set;}
    public string FullName {get;set;}
    public string PartialName { get; set; }
}

This one seems the most flexible (in the sense that it would work with either Linq, Plain Old SQL.

But it just stinks of "Writing your own query language" because you need to account for every possible query the consumer might want to do. e.g.
Age >= 18 && Age <=65 && Name LIKE '%John%'

Option 3.

Are there any other options ?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

罪#恶を代价 2024-12-18 22:19:55
  1. 您的说法“如果您的实现必须使用手工制作的存储过程/sql/ADO.NET 等,那么选项 1 似乎会失败”是不正确的。通过使用 ExpressionVisitor 实现解释查询表达式,完全可以生成与选项 2 中的模型类似的模型。

  2. 如果您要从 LINQ Search 方法返回 IEnumerable 而不是 IQueryable,您应该纳入某种机制来支持 (1) 分页 和 (2) 排序

  3. 我真的不喜欢选项#2。它没有明确说明您正在搜索的内容。例如,如果年龄为空,这是否意味着您正在搜索年龄为空的用户,或者您忽略了年龄参数?如果 Name 和 PartialName 都指定了怎么办?使用 LINQ 方法,您可以执行诸如 StartsWithContainsEquals 等操作。

  4. 存储库模式实现应该抽象出来数据访问逻辑,并公开面向业务的接口。通过使用通用接口(Expression>)直接访问存储库,您会损失一点,因为该接口没有向调用者传达意图。有几种方法可以做得更好。

    • 实现基于 LINQ 表达式的规范模式 创建更强类型的查询。因此,您可以使用 这样的规范语法,而不是通过 Search(person => person.Age.HasValue && person.Age.Value > 18) 查询成人。 Search(new PersonIsAdultSpecification());,其中规范包装了底层 LINQ 表达式,但公开了面向业务的接口。
      • 就我个人而言,我喜欢这种方法,但是Ayende 称之为“末日深渊中的架构”,因为它很容易导致过度设计。他的替代建议是将这样的特定查询包装在扩展方法中。我认为这可能同样可行,但我更喜欢使用强类型对象。
      • 最简单的方法是实际声明您将在 IPersonRepository 接口上执行的实际查询。因此,该接口实际上会声明一个 SearchForAdults() 方法。

一般来说,每当您查询数据库时,您都应该刻意尝试获取某些数据。每当您查询存储库时,您都应该刻意尝试获取满足特定业务约束的业务对象。为什么不让这个业务约束更加明确呢?为任何服务定义一个好的接口取决于该服务的使用者。没有通用的神奇子弹存储库接口,但您可以根据您的特定应用程序创建更好或更差的接口。这个想法是要记住为什么您首先要使用存储库模式,那就是简化逻辑并创建更直观的访问点。

  1. Your statement that "option 1 seems to fall down though if you're implementation has to use hand crafted stored procs/sql/ADO.NET etc" is incorrect. Is is completely possible to generate a model just like the one in option 2, by means of interpreting the query expression with an ExpressionVisitor implementation.

  2. If you are going to return an IEnumerable<T> instead of an IQueryable<T> from the LINQ Search method, you should incorporate some mechanism to support (1) paging, and (2) sorting.

  3. I really dislike option #2. It does not make explicit what you are searching for. e.g. If the age is null, does that mean you are searching for users with null ages, or you are ignoring the age parameter? What if Name and PartialName are both specified? With the LINQ approach you can do stuff like StartsWith, Contains, Equals, etc.

  4. A repository pattern implementation is supposed to abstract away the data access logic, and expose a business-oriented interface. By directly accessing the repository with a generic interface (Expression<Func<Person,bool>>), you are losing out on this a little bit, because the interface does not convey the intention to the caller. There are several ways to do this better.

    • Implementing a LINQ expression-based Specification Pattern creates more strongly typed queries. So, instead of querying for adults by Search(person => person.Age.HasValue && person.Age.Value > 18), you would use a specification syntax like Search(new PersonIsAdultSpecification());, where the specification wraps the underlying LINQ expression, but exposes a business-oriented interface.
      • Personally, I like this approach, but Ayende calls it 'architecting in the pit of doom', because it can easily lead to over-engineering. His alternative suggestion is to wrap specific queries like this in extension methods. I think this is probably equally viable, but I prefer having the strongly-typed object.
      • The easiest way to do this would be to actually declare the realistic queries you will perform on the IPersonRepository interface. So, the interface would actually declare a SearchForAdults() method.

In general, Whenever you are querying the database, you should be deliberately trying to get certain data. Whenever you query the repository, you should be deliberately trying to get business objects that satisfy a certain business constraint. Why not make this business constraint more explicit? Defining a good interface for any service depends on the consumer of that service. There isn't a generic magic bullet repository interface, but you can create a better or worse one in context of your specific application. The idea is to remember why you are using the repository pattern in the first place, and that is to simplify the logic and create a more intuitive access point.

阳光①夏 2024-12-18 22:19:55

我不确定在这种情况下是否存在“官方来源”之类的东西,但我确实领导了 LINQ for Visual Basic 的设计,所以也许这很重要。无论如何,这是我的看法:

您似乎提出的问题是您希望能够将谓词表达式传输到后端“无论[后端]实现如何”。然而,在我看来,您列出的两个选项实际上只是彼此的变体,在这两种情况下,后端都必须能够理解传入的谓词规范。选项一恰好更通用和灵活比选项二(但翻译起来也更痛苦)。选项二比选项二受到更多限制,但处理结构更简单(尽管当您允许在第二个选项中指定越来越复杂的查询时,结构将不可避免地越来越类似于第一个选项的结构)。

最重要的是,你的问题没有好的答案。不存在可以确保所有提供程序都支持的通用表达式结构,因此,如果您确实希望拥有一个适用于任何提供程序(无论实现如何)的搜索方法,那么您如果提供者不支持(例如)表达式树,则必须愿意在本地运行谓词。根据具体情况,这可能适合您,也可能不适合您。

另一方面,如果(更有可能)您将收到的可能提供者的列表是有限的,您可能最好决定什么对您最重要(易于使用/可表达性与易于实施)并选择您想要对提供商施加的限制。对于消费者应用程序程序员来说,表达式树在很多方面肯定是最好的,所以如果你认为你可以摆脱这个限制,那就去吧。否则,某种更简单的自定义结构可能会给您带来更少的痛苦和心痛。 (我不建议尝试自己翻译表达式树,除非您有大量的额外时间和耐心。)

I'm not sure there is such a thing as an "offical source" in this case, but I did lead the design of LINQ for Visual Basic so maybe that counts for something. Anyway here's my take on it:

The problem you seem to be posing is that you want to be able to transmit a predicate expression to a backend "regardless of the [backend] implementation." However, it seems to me that the two options you list are really just variants of each other, in that in both cases the backend has to be able to understand the predicate specification that's being passed in. Option one happens to be more general and flexible than option two (but it is also much more of a pain to translate). Option two is more restricted than option two but is a simpler structure to deal with (although as you allow more and more complex queries to be specified in the second option, the structure will inevitably come more and more to resemble the structure from the first option).

The bottom line is that there is no good answer to your question. There is no universal expression structure that you can be sure all providers will support, so if you really want to have a Search method that works over any provider regardless of implementation, you'd have to be willing to fall back to running the predicate locally if the provider doesn't support, say, expression trees. Depending on the scenario, that may or may not work for you.

On the other hand, if (as is more likely) the list of possible providers you'll receieve is finite, you'd probably be better off deciding what is most important to you (ease of use/expressibility vs. ease of implementation) and pick what constraints you want to put on the providers. Expression trees would certainly be the nicest in many ways for the consumer application programmer, so if you think you can get away with that restriction go for it. Otherwise, some sort of simpler custom structure will probably cause you less pain and heartache. (I don't recommend trying to translate expression trees yourself unless you've got a lot of extra time and patience.)

寄与心 2024-12-18 22:19:55

关于存储库模式,一直困扰我的一件事是,当涉及到分组、聚合计数等时,它缺乏灵活性。这就是为什么我决定还提供一个单独的接口(例如 IExtendedRepository),它带有一个有趣的方法签名,如下所示:

TResult Aggregate<TResult>(Func<IQueryable<TEntity>, TResult> aggregatorFunc);

此方法是一个有点怪物的工具,您可以通过多种巧妙的方式使用它:

var countSomething = repository.Aggregate(x => x.Where(y => y.SomeProperty).Count());

请注意(取决于您的存储库实现) )Count() 调用将编译为 SQL Count,因此它不是内存中的 Count()。

One thing that always bothered me about the Repository pattern is it's inflexibility when it comes down to things like grouping, aggregating counts and so on. That's why I decided to also provide a seperate Interface (e.g. IExtendedRepository) that comes with a funcy method signature like this:

TResult Aggregate<TResult>(Func<IQueryable<TEntity>, TResult> aggregatorFunc);

This method is a bit of a monster tool which you can use in many neat ways:

var countSomething = repository.Aggregate(x => x.Where(y => y.SomeProperty).Count());

Notice that (depending on your repository implementation) the Count() call will compile down into an SQL Count, so it's NOT an in-memory Count().

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文