增删改是Respository的职责,还是Entity的职责?
如果要删除指定标识的实体,是由Respository来操作,还是Entity本身来操作?
从业务角度来看,入库、出库都需要记录,删除操作自然应该放在Respository。可是Respository并不知道Entity的细节,如何执行删除操作?
难道是要Respository创建一条消息,然后将消息依次传递给相关的Entity,Entity收到消息后删除自己?
如果要删除指定标识的实体,是由Respository来操作,还是Entity本身来操作?
从业务角度来看,入库、出库都需要记录,删除操作自然应该放在Respository。可是Respository并不知道Entity的细节,如何执行删除操作?
难道是要Respository创建一条消息,然后将消息依次传递给相关的Entity,Entity收到消息后删除自己?
在CQRS里Repository是不允许有删除的功能,为得能使领域模型可以还原到任何时间点,所以不允许删除,你只能通过Repository加载一个实体或增加一个实体,有些同仁不同意实体自己删除自己,认为这是自杀行为,我觉着一个实体可以有删除行为,但是由别人去触发删除动作,这样实体会产生一个EntityDeletedEvent的事件,由事件Handler负责删除数据
这个世界上是没有真正意义上的删除操作的,当你在做一个删除操作时,要多问问自己,什么是删除?删除意味着什么?你能做的最多是个逻辑删除,但是逻辑删除也往往伴随着一定的业务含义,如撤销,销毁,禁用,移到垃圾箱,etc
实体永远无法删除自己,实体能自己删除自己就等于一个人突然从宇宙中消失。
也许你会说,人可以自杀呀。但是我们首先要清楚人的自杀是什么含义,应该只是让自己永远没有意识,让自己认为从这个世界上已经消失。但这不表示你真的从这个世界上消失了,世界上你还在,只有在火葬场的人把你火化了,你才真正意义上的消失了,从物理学角度来讲,此时你依然没消失,呵呵,因为你的原子还在。当然这个有点扯远了。
人可以自杀是因为他可以知道该如何让自己无法呼吸,比如咬舌自尽,割脉,投河自尽,等等。那么想想人为什么能有这种自杀的能力?因为他知道自己自杀需要哪些措施和信息。比如要咬舌,他自己知道只需要让自己的舌头断掉即可,要割脉只需要那把刀让自己的血液流光即可;这里,我们可以理解为,人的舌头,血液都是人这个对象的子对象,人因为拥有这些子对象,自然就可以控制和修改它们的状态。从这点来看,我们DDD内存中的一个对象,也可以自己修改自己的状态,但是他却无法让自己从内存消失。从内存消失才是等于人从宇宙消失,对吧?所以,你觉得Entity能自己删除自己吗?显然不能!前面我说道的撤销,销毁,禁用,转移到垃圾箱,其实都只是修改了一下实体自己的某个状态而已。并不是真正的删除。
只有独立于对象之外的其他对象才有可能删除它,就像只有另外一个人才能让你从这个世界上消失一样。那么谁能把对象从内存或数据库删除?很简单,就看你想把该对象从哪里移除?如果是从数据库移除,那就让负责处理数据库的那个类(可能就是你说的repository了)来做这个事情,一条delete sql即可;如果是in-memory模式,那可能就是将对象从某个集合移除。
个人感觉这是有史以来我说明为什么对象不能自己删除自己的最生动的一次解释了吧,哈哈!
其实,现在想来,我们不应该用”职责“,职责听起来有点带有”目的性“,更好一点不会让人产生更多歧义的说法应该是”是entity自己有能力删除自己还是repository有能力删除entity?“.这个问题再结合我上面的回复,相信就很好理解了。
我们谈论一个对象时,我觉得更多的应该是表达”对象有哪些能力“,或者”对象有哪些行为“,这两种说法都可以。但是对象有哪些职责,容易让人误解为能有意识的主动的去行驶这些能力,实际上对象暴露在外部的能力一般都是其他对象驱动然后去行驶的,比如repository.remove(entity)这个方法,表示repository有能力从它所维护的存储空间删除/移除一个entity,但并不表示repository会主动删除一个给定的entity,实际上一定是更上层的某个对象去通知repository行驶remove的能力。
"增删改"这个词语表达全面了是:“对XXX中的数据增删改”,本质上是一种DAO模式,或者说是数据库操作思考方式,而不是围绕业务模型的操作思考方式。
"增删改"在不同的业务场景下可以用其他词语替代:如增加帖子,实际是一种post动作,然后触发一个postedEvent事件。
"增删改"是技术黑客们不管业务场景,强制总结出来的一套模式用语,他们认为不管你业务千变万化,总是要用数据库,一夫当关,万夫莫开,就像REST中POST/PUT/GET/DEL总是被人对应解释为增删改一样。
但是这种习惯思维无法改变,就别当真钻进去探究个结果来,因为问题方向本身就可能不对。
也许可以换个说法,我们可以向仓储发出请求,获取若干实体,实体负责跟踪自身状态的变化,然后将之持久化。
using(DataContext context) 初始化场景
{
Entity entity = context.Respository<T>.Get() 获取到实体
entity.SetData() 更改实体状态
context.SubmitChanges() 持久化更改
}
现在问题来了,context.SubmitChanges这个消息需要经过Respository么?
第一种实现:
DataContext -> Respository(这里操作DAO) -> Entity(更新IsDirty标识或内存状态)
第二种实现:
DataContext -> Entity(这里操作DAO) -> Respository(更新IsDirty标识或内存状态)
上面的箭头表示消息的传递路径,而不是直接的调用关系
个人比较喜欢第二种实现,仓储的职责仅仅是把实体拿出来或者放回去,而不操作实体内的数据。
就像看车库的门卫,他只要登记车辆进出的状态,而不需要修理汽车的内部零件。
第二种实现也很好的解决了CQRS批量操作的性能问题这个贴子提出的疑问:
1.context.Respository<T>.Get()获取到的仅仅是一个包含贴子标识符匹配规则的EntityCollection,并没有真正的去查询数据库。
2.entity.SetData()将这些EntityCollection标记为无效
3.context.SubmitChanges() 提交SQL更新,这个操作是由EntityCollection调用DAO实现的
4.Respository得到EntityDeleted的消息
实在看不懂gameboyLV想表达的意思
上下文场景,场景负责创建领域内的仓储,向仓储发送消息。
此处为了省略,使用PendingChanges实体类替代消息,ISubmitable接口替代消息处理器。
|
|
|
图书的仓储,可以获取指定ID的图书实体,或是指定查询的图书实体集合
此处为了省略,使用EntityIdentifier代替查询对象
|
|
|
图书的实体,容纳一本图书
|
图书的实体集合,容纳一堆图书
|
大功告成,既能批量操作,又能单独操作,还支持事务和消息。
|
数据库操作部分具体写在哪要看你采取哪种方式了.
数据库操作方式一般是:
1:表入口模式
2:行入口模式
3:活动记录
4:数据Mapping方式
一般Entity采用2,3,4模式. 2,3模式区别在于2加上领域模型就变成3.2,3模式就相当于把数据操作写入entity里. 4模式会把增删改查写入mapping.Respository或者DAO通过mapping对数据库操作.