检测聚合根内实体的变化
我想看看人们可能采取什么方法来检测作为其聚合一部分的实体的变化。我有一些有用的东西,但我并不热衷于它。基本上,我的存储库负责确定聚合根的状态是否已更改。假设我有一个名为 Book
的聚合根和一个名为 Page
的实体。一本书包含一个或多个页面实体,存储在页面集合中。
主要是,插入与更新场景是通过检查聚合根及其实体以确定键是否存在来完成的。如果该键存在,则假定该对象已一次保存到底层数据源。这使其成为更新的候选者;但仅凭这一点对于实体来说并不是确定的。对于聚合根,答案是显而易见的,因为只有一个并且它是入口的奇异点,因此可以假设密钥的存在将决定操作。就我而言,这是一个可以接受的方案,再次保存聚合根本身,以便我可以捕获修改日期。
为了帮助促进实体本身的这种行为,我的 EntityBase
类包含两个简单的属性:IsUpdated()
、IsDeleted()
。这两个都默认为 false。我不需要知道它是否是新的,因为我可以根据密钥的存在来做出决定,如前所述。实现上的方法(在本例中为 Page)将具有将支持数据集 IsUpdated()
更改为 true 的每个方法。
例如,Page 有一个名为 UpdateSectionName()
的方法,它可以更改 SectionName
属性的支持值,该属性是只读的。这种方法的使用是一致的,因为它允许执行该数据设置的方法中验证器的逻辑连接点(防止实体进入无效状态)。最终结果是我必须在方法末尾添加 this.IsUpdated() = true;
。
当聚合根发送到存储库以进行 Save()
时(逻辑切换到 Insert()
或 Update()
操作),然后它可以迭代 Book
中的 Pages
集合,查找具有以下三种情况之一的任何页面:
- 无键。将插入一个没有键的
Page
。 IsDeleted = true;
删除胜过更新,并且删除将被提交 - 忽略页面
的任何更新。IsUpdated = true;
将提交页面更新。
这样做可以防止我盲目地更新 Pages 集合中的所有内容,例如,如果书中有数百个 Page 实体,这可能会令人畏惧。我一直在考虑检索这本书的副本,进行比较并仅提交检测到的更改(根据存在和/或比较插入、更新和删除),但这似乎是一种非常闲聊的方式。
主要缺点是开发人员必须记住在实体的每个方法中设置 IsUpdated。忘记一个,它将无法检测到该值的变化。我曾考虑过某种自定义后备存储的想法,它可以透明地为更改添加时间戳,这反过来又可以使 IsUpdated
成为存储库可用于聚合更新的只读属性。
存储库正在使用工作单元模式实现,该实现的操作基于将聚合根添加到其中时生成的时间戳。由于可能有多个实体排队等待操作,因此在执行实体所属的聚合根操作后立即汇总并执行实体操作。我可以看到更进一步,创建另一个工作单元来处理实体操作,并将它们基于实体中使用的某种事件跟踪(这就是我假设市场上的一些 ORM 产品实现的方式)类似级别的功能)。
不过,在我继续朝这个方向前进之前,我很想听听有关此的想法/建议/经验。
编辑:一些可能有助于了解的附加信息:
- 我当前使用的语言是 C#,尽管我试图尽可能多地保留特定于语言的信息,因为这更多的是理论讨论。
- 存储库/服务/实体/等的代码。基于 Tim McCarthy 在他的书“.NET Domain-Driven Design with C#”中的概念以及 CodePlex。它提供了对所采用方法类型的可运行的理解,尽管我正在使用的内容很大程度上是从头开始重写的。
I am looking to see what approaches people might have taken to detect changes in entities that are a part of their aggregates. I have something that works, but I am not crazy about it. Basically, my repository is responsible for determining if the state of an aggregate root has changed. Let's assume that I have an aggregate root called Book
and an entity called Page
within the aggregate. A Book
contains one or more Page
entities, stored in a Pages
collection.
Primarily, insert vs. update scenarios are done by inspecting the aggregate root and its entities to determine the presence of a key. If the key is present, it is presumed that the object has been, at one time, saved to the underlying data source. This makes it a candidate for an update; but it is not definitive based upon that alone for the entities. With the aggregate root the answer is obvious, since there is only one and it is the singular point of entry, it can be assumed that key presence will dictate the operation. It is an acceptable scenario, in my case, to save the aggregate root itself back again so that I can capture a modification date.
To help facilitate this behavior for the entities themselves, my EntityBase
class contains two simple properties: IsUpdated()
, IsDeleted()
. Both of these default to false. I don't need to know if it is new or not, because I can make that determination based upon the presence of the key, as mentioned previously. The methods on the implementation, in this case the Page, would have each method that changes the backing data set IsUpdated()
to true.
So, for example, Page has a method called UpdateSectionName()
which changes the backing value of the SectionName
property, which is read-only. This approach is used consistently, as it allows for a logical attachment point of validators in the method (preventing the entity from entering an invalid state) that performs that data setting. The end result is that I have to put a this.IsUpdated() = true;
at the end of the method.
When the aggregate root is sent into the repository for the Save()
(a logic switch to either an Insert()
or Update()
operation), it can then iterate over the Pages
collection in the Book
, looking for any pages that have one of three scenarios:
- No key. A
Page
with no key will be inserted. IsDeleted = true;
A delete trumps an update, and the deletion will be committed - ignoring any update for thePage
.IsUpdated = true;
An update will be committed for the Page.
Doing it this way prevents me from just blindly updating everything that is in the Pages collection, which could be daunting if there were several hundred Page entities in the Book, for example. I had been considering retrieving a copy of the Book, and doing a comparison and only committing changes detected, (inserts, updates, and deletes based upon presence and/or comparison), but it seemed to be an awfully chatty way to go about it.
The main drawback is that the developer has to remember to set IsUpdated in each method in the entity. Forget one, and it will not be able to detect changes for that value. I have toyed with the idea of some sort of a custom backing store that could transparently timestamp changes, which could in turn make IsUpdated
a read-only property that the repository could use to aggregate updates.
The repository is using a unit of work pattern implementation that is basing its actions on the timestamp generated when the aggregate root was added to it. Since there might be multiple entities queued for operations, entity operations are rolled up and executed immediately after the aggregate root operation(s) are executed that the entities belong to. I could see taking it a step further and creating another unit of work to just handle the entity operations and base them off some sort of event tracking used in the entity (which is how I am assuming that some of the ORM products on the market accomplish a similar level of functionality).
Before I keep on moving in this direction, though, I would love to hear ideas/recommendations/experiences regarding this.
Edit: A few additional pieces of information that might be helpful to know:
- The current language that I am working with is C#, although I tried to keep as much language-specific information out as possible, because this is more of a theoretical discussion.
- The code for the repositories/services/entities/etc. is based upon Tim McCarthy's concept in his book, ".NET Domain-Driven Design with C#" and the supporting code on CodePlex. It provides a runnable understanding of the type of approach taken, although what I am working with has largely been rewritten from the ground up.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
简而言之,我的回答是我按照我的建议去做。它正在发挥作用,尽管我确信还有改进的空间。这些改变实际上只花了很少的时间,所以我觉得在这种情况下我并没有偏离 KISS 或 YAGNI 原则太远。 :-)
我仍然觉得操作上的计时相关问题还有空间,但我应该能够在存储库实现中解决它们。这不是理想的解决方案,但我不确定是否值得重新发明轮子来纠正一个可以在比修复时间更短的时间内避免的问题。
In short, my answer is that I went with what I proposed. It is working, although I am sure that there is room for improvement. The changes actually took very little time, so I feel I didn't navigate too far from the KISS or YAGNI principals in this case. :-)
I still feel that there is room for timing related issues on operations, but I should be able to work around them in the repository implementations. Not the ideal solution, but I am not sure that it is worth reinventing the wheel to correct a problem that can be avoided in less time than it takes to fix.