存储库模式最佳实践

发布于 2024-08-03 17:06:07 字数 306 浏览 5 评论 0原文

因此,我在应用程序中实现存储库模式,并在我对该模式的理解中遇到了两个“问题”:

  1. 查询 - 我读到了在使用存储库时不应使用 IQueryable 的回复。但是,很明显您希望每次调用方法时都不会返回完整的对象列表。是否应该实施?如果我有一个名为 List 的 IEnumerable 方法,那么 IQueryable 的一般“最佳实践”是什么?它应该/不应该有哪些参数?

  2. 标量值 - 返回单个标量值而不必返回整个记录的最佳方法(使用存储库模式)是什么?从性能的角度来看,在整行中仅返回单个标量值不是更有效吗?

So I'm implementing the repository pattern in an application and came across two "issues" in my understanding of the pattern:

  1. Querying - I've read responses that IQueryable should not be used when using repositories. However, it's obvious that you'd want to so that you are not returning a complete List of objects each time you call a method. Should it be implemented? If I have an IEnumerable method called List, what's the general "best practice" for an IQueryable? What parameters should/shouldn't it have?

  2. Scalar values - What's the best way (using the Repository pattern) to return a single, scalar value without having to return the entire record? From a performance standpoint, wouldn't it be more efficient to return just a single scalar value over an entire row?

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

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

发布评论

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

评论(3

和影子一齐双人舞 2024-08-10 17:06:07

严格来说,存储库提供用于获取/放置域对象的集合语义。它提供了围绕物化实现(ORM、手动、模拟)的抽象,以便域对象的使用者与这些细节解耦。在实践中,存储库通常抽象对实体的访问,即具有身份的域对象,并且通常是持久生命周期(在 DDD 风格中,存储库提供对聚合根的访问)。

存储库的最小界面如下:

void Add(T entity);
void Remove(T entity);
T GetById(object id);
IEnumerable<T> Find(Specification spec);

尽管您会看到命名差异以及 Save/SaveOrUpdate 语义的添加,但以上是“纯粹”的想法。您将获得 ICollection 添加/删除成员以及一些查找器。如果您不使用 IQueryable,您还会在存储库中看到查找器方法,例如:

FindCustomersHavingOrders();
FindCustomersHavingPremiumStatus();

在此上下文中使用 IQueryable 存在两个相关问题。第一个是可能以域对象关系的形式向客户端泄露实现细节,即违反德米特定律。第二个是存储库获得了查找可能不属于域对象存储库本身的职责,例如,查找与所请求的域对象相关而不是与相关数据相关的投影。

此外,使用 IQueryable “打破”了这种模式:具有 IQueryable 的存储库可能会也可能不会提供对“域对象”的访问。 IQueryable 为客户端提供了很多关于最终执行查询时将具体化什么的选项。这是有关使用 IQueryable 的争论的主旨。

关于标量值,您不应使用存储库来返回标量值。如果您需要标量,通常可以从实体本身获取它。如果这听起来效率低下,确实如此,但您可能不会注意到,具体取决于您的负载特征/要求。如果由于性能原因或需要合并来自多个域对象的数据而需要域对象的备用视图,则有两种选择。

1) 使用实体的存储库查找指定的实体并将其投影/映射到扁平化视图。

2) 创建一个专用于返回封装所需扁平视图的新域类型的查找器接口。这不会是一个存储库,因为不会有集合语义,但它可能会在幕后使用现有的存储库。

如果您使用“纯”存储库来访问持久化实体,需要考虑的一件事是您会损害 ORM 的一些好处。在“纯”实现中,客户端无法提供如何使用域对象的上下文,因此您无法告诉存储库:“嘿,我只是要更改 customer.Name 属性,所以不要这样做”不必费心去获取那些急切的参考资料。另一方面,问题是客户是否应该了解这些东西。这是一把双刃剑。

就使用 IQueryable 而言,大多数人似乎都愿意“打破”模式以获得动态查询组合的好处,特别是对于分页/排序等客户端职责。在这种情况下,您可能会:

Add(T entity);
Remove(T entity);
T GetById(object id);
IQueryable<T> Find();

然后您可以取消所有这些自定义 Finder 方法,随着查询需求的增长,这些方法确实会使存储库变得混乱。

Strictly speaking, a Repository offers collection semantics for getting/putting domain objects. It provides an abstraction around your materialization implementation (ORM, hand-rolled, mock) so that consumers of the domain objects are decoupled from those details. In practice, a Repository usually abstracts access to entities, i.e., domain objects with identity, and usually a persistent life-cycle (in the DDD flavor, a Repository provides access to Aggregate Roots).

A minimal interface for a repository is as follows:

void Add(T entity);
void Remove(T entity);
T GetById(object id);
IEnumerable<T> Find(Specification spec);

Although you'll see naming differences and the addition of Save/SaveOrUpdate semantics, the above is the 'pure' idea. You get the ICollection Add/Remove members plus some finders. If you don't use IQueryable, you'll also see finder methods on the repository like:

FindCustomersHavingOrders();
FindCustomersHavingPremiumStatus();

There are two related problems with using IQueryable in this context. The first is the potential to leak implementation details to the client in the form of the domain object's relationships, i.e., violations of the Law of Demeter. The second is that the repository acquires finding responsibilities that might not belong to the domain object repository proper, e.g., finding projections that are less about the requested domain object than the related data.

Additionally, using IQueryable 'breaks' the pattern: A Repository with IQueryable may or may not provide access to 'domain objects'. IQueryable gives the client a lot of options about what will be materialized when the query is finally executed. This is the main thrust of the debate about using IQueryable.

Regarding scalar values, you shouldn't be using a repository to return scalar values. If you need a scalar, you would typically get this from the entity itself. If this sounds inefficient, it is, but you might not notice, depending on your load characteristics/requirements. In cases where you need alternate views of a domain object, because of performance reasons or because you need to merge data from many domain objects, you have two options.

1) Use the entity's repository to find the specified entities and project/map to a flattened view.

2) Create a finder interface dedicated to returning a new domain type that encapsulates the flattened view you need. This wouldn't be a Repository because there would be no Collection semantics, but it might use existing repositories under the covers.

One thing to consider if you use a 'pure' Repository to access persisted entities is that you compromise some of the benefits of an ORM. In a 'pure' implementation, the client can't provide context for how the domain object will be used, so you can't tell the repository: 'hey, I'm just going to change the customer.Name property, so don't bother getting those eager-loaded references.' On the flip side, the question is whether a client should know about that stuff. It's a double-edged sword.

As far as using IQueryable, most people seem to be comfortable with 'breaking' the pattern to get the benefits of dynamic query composition, especially for client responsibilities like paging/sorting. In which case, you might have:

Add(T entity);
Remove(T entity);
T GetById(object id);
IQueryable<T> Find();

and you can then do away with all those custom Finder methods, which really clutter the Repository as your query requirements grow.

手长情犹 2024-08-10 17:06:07

作为对@lordur的回应,我真的不喜欢指定存储库接口的事实上的方法。

因为解决方案中的接口要求每个存储库实现至少需要一个 Add、Remove、GetById 等。现在考虑一个场景,其中通过存储库的特定实例进行保存没有意义,您仍然必须实现其余方法带有 NotImplementedException 或类似的内容。

我更喜欢像这样分割我的存储库接口声明:

interface ICanAdd<T>
{
    T Add(T entity);
}

interface ICanRemove<T>
{
    bool Remove(T entity);
}

interface ICanGetById<T>
{
    T Get(int id);
}

SomeClass 实体的特定存储库实现可能如下所示:

interface ISomeRepository
    : ICanAdd<SomeClass>, 
      ICanRemove<SomeClass>
{
    SomeClass Add(SomeClass entity);
    bool Remove(SomeClass entity);
}

让我们退后一步,看看为什么我认为这是比实现所有 CRUD 方法更好的实践一个通用接口。

有些对象有不同的
要求比别人高。一位顾客
对象不能被删除,a
无法更新采购订单,并且
ShoppingCart 对象只能是
创建的。当一个人使用通用的
IRepository接口这个
显然会导致问题
执行。

那些实施反模式的人
通常会充分实施
然后接口会抛出异常
对于他们不知道的方法
支持。除了不同意
这违反了许多面向对象的原则
他们希望能够利用他们的
IRepository有效抽象
除非他们也开始放置方法
无论是否给定对象
予以支持并进一步落实
他们。

此问题的常见解决方法是
转向更精细的界面
例如 ICanDelete、ICanUpdate、
ICanCreate 等等等此时
解决许多问题
在 OO 方面兴起的
原理也大大减少了
正在重用的代码量
大多数时候人们不会
能够使用存储库
不再具体实例。

我们都不喜欢编写相同的代码
一遍又一遍。然而一个存储库
如同建筑接缝一样收缩
是错误的地方来扩大
合同以使其更加通用。

这些摘录无耻地摘自这篇文章 您还可以在评论中阅读更多讨论。

In response to @lordinateur I don't really like the defacto way to specify a repository interface.

Because the interface in your solution requires that every repository implementation requires at least an Add, Remove, GetById, etc.. Now consider a scenario where it doesn't make sense to Save through a particular instance of a repository, you still have to implement the remaining methods with NotImplementedException or something like that.

I prefer to split my repository interface declarations like so:

interface ICanAdd<T>
{
    T Add(T entity);
}

interface ICanRemove<T>
{
    bool Remove(T entity);
}

interface ICanGetById<T>
{
    T Get(int id);
}

A particular repository implementation for a SomeClass entity might thus look like the following:

interface ISomeRepository
    : ICanAdd<SomeClass>, 
      ICanRemove<SomeClass>
{
    SomeClass Add(SomeClass entity);
    bool Remove(SomeClass entity);
}

Let's take a step back and take a look at why I think this is a better practice than implementing all CRUD methods in one generic interface.

Some objects have different
requirements than others. A customer
object may not be deleted, a
PurchaseOrder cannot be updated, and a
ShoppingCart object can only be
created. When one is using the generic
IRepository interface this
obviously causes problems in
implementation.

Those implementing the anti-pattern
often will implement their full
interface then will throw exceptions
for the methods that they don’t
support. Aside from disagreeing with
numerous OO principles this breaks
their hope of being able to use their
IRepository abstraction effectively
unless they also start putting methods
on it for whether or not given objects
are supported and further implement
them.

A common workaround to this issue is
to move to more granular interfaces
such as ICanDelete, ICanUpdate,
ICanCreate etc etc. This while
working around many of the problems
that have sprung up in terms of OO
principles also greatly reduces the
amount of code reuse that is being
seen as most of the time one will not
be able to use the Repository
concrete instance any more.

None of us like writing the same code
over and over. However a repository
contract as is an architectural seam
is the wrong place to widen the
contract to make it more generic.

These exerpts have been shamelesly taken from this post where you can also read more discussion in the comments.

朮生 2024-08-10 17:06:07

关于1:
据我所知,从存储库返回的问题不是 IQuerable 本身。存储库的要点是它应该看起来像一个包含所有数据的对象。所以你可以向存储库询问数据。如果您有多个对象需要相同的数据,则存储库的工作就是缓存数据,因此存储库的两个客户端将获得相同的实例 - 因此,如果一个客户端更改属性,另一个客户端将看到该数据,因为它们指向同一个实例。

如果存储库实际上是 Linq 提供程序本身,那么这就很合适。但大多数人只是让 Linq-to-sql 提供程序的 IQuerable 直接通过,这实际上绕过了存储库的责任。所以存储库根本不是存储库,至少根据我对模式的理解和使用。

关于2:
当然,仅从数据库返回单个值比返回整个记录更具性能效率。但是使用存储库模式,您根本不会返回记录,而是返回业务对象。因此应用程序逻辑不应该关心字段,而应该关心域对象。

但是,与完整的域对象相比,返回单个值有多有效呢?如果您的数据库模式定义得相当好,您可能无法衡量差异。

拥有干净、易于理解的代码比预先进行微观性能优化要重要得多。

Regarding 1:
As far as I can see it, it is not the IQuerable itself that is the problem being returned from a repository. The point of a repository is that it should look like an object that contains all your data. So you can ask the repository for the data. If you have more than one object needing the same data, the job of the repository is to cache the data, so the two clients of your repository will get the same instances - so if the one client changes a property, the other will see that, becuase they are pointing to the same instance.

If the repository was actually the Linq-provider itself, then that would fit right in. But mostly people just let the Linq-to-sql provider's IQuerable pass right through, which in effect bypasses the responsibility of the repository. So the repository isn't a repository at all, at least according to my understanding and usage of the pattern.

Regarding 2:
Naturally it is more performance-effective to just return a single value from the database than the entire record. But using a repository pattern, you wouldn't be returning records at all, you would be returning business objects. So the application logic should not concern itself with fields, but with domain objects.

But how more effective is it to return a single value compared to a complete domain object? You will probably not be able to measure the difference if your database schema is reasonably well defined.

It is a lot more important to have clean, easy-to-understand code - instead of microscopic performance optimizations up front.

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