说到DDD难,我觉得主要是两点:建模难、代码落地难。前者需要业务熟、功力深,难以快速提升;后者难在缺乏简单易行的可参考的代码结构,一旦有了这样的参考结构,就可以快速大幅降低DDD的实践难度。本文从后者的诸多难点中选择一个最常见的问题进行探讨:如何优雅地实现聚合的持久化?

持久化什么,谁来做持久化?

我们先来看一下,以前我们是怎么做持久化的。下图是一个非常典型的三层架构,业务逻辑层相当于Service层,完成业务处理。持久化主要通过数据访问层完成,这一层会有ORM、DAO等。这时候的实体类是一个JavaBean, 没有行为,它是一个贫血模型。我们会在实体类上加上注解,用来做数据库的持久化。这是非常典型的以数据为中心的编程方式,可能大家开始学编程时就是这么做的,非常成熟,没有什么挑战。

但系统复杂度达到一定程度时,以数据为中心的编程方式就会导致业务逻辑散布在各个地方,系统变得很难维护,响应力越来越低。因此Eric提出了领域驱动设计方法。其中一个重要的手段就是分离关注点,将技术复杂度与业务复杂度分离。也就是说,业务代码应该只关注业务逻辑的实现,不需要关心对象如何被持久化到数据库。持久化属于技术实现,与业务无关,它不需要知道业务是如何处理的。因此Eric提出了DDD的分层架构,如下图。其中一个主要的区别在于,我们将业务逻辑从Service中移出来,放在了Domain model中,变成了充血模型,封装在聚合中,应用层Service只是起协调作用。同时,将数据访问层不见了,都放在基础设施层了。

那么这时候我们聊持久化的时候,我们持久化的是什么,谁来做持久化?Eric帮我们给出了定义。我们持久化的是聚合。

聚合(Aggregate)就是一组相关对象的集合,我们把它作为数据修改的单元。每个聚合都有一个根(Root)和一个边界( Boundary) ——Eric Evans

为什么数据修改的单元是聚合而不是实体或者其它东西呢?因为聚合封装了业务规则,也就是“不变性(Invariants)”,因此我们要把它作为一个整体而不是拆开来,以确保业务规则不被破坏。所以我们要以聚合为单元进行持久化。谁来做这个持久化的工作呢?资源库Repository。以订单为例,这是一个典型的Repository接口定义。

public interface OrderRepository {public Order findById(String id);public void save(Order order);public void remove(Order order);
}

领域模型和持久化模型要分离吗?

当我们做持久化的时候,问题来了,我们需要单独的持久化模型吗?或者说,领域模型和持久化模型要分离吗?当你做持久化时,是直接拿领域模型做持久化,还是转换成持久化模型后做数据库保存。我遇到很多团队在这个问题上有分歧。有的说我这个系统的领域模型和持久化模型基本是一样的,没必要分离。另一部分观点说,这两种模型的职责是不一样的,应该分离,这样它们才能够分别独立演进。这两种听起来都很有道理。怎么选择?

我认为,应该分离这两种模型。原因非常简单,如果不分离,你的领域模型必然要为了持久化而妥协。比如说,你在设计领域模型时,要考虑如何保存到数据库中。更为糟糕的是,你还要满足ORM框架的要求,你要有空的构造方法,还要加上各种Setter。当你妥协完了后,你如何确保值对象是只读的?当你的属性很容易就被Set方法改变时,你如何封装你的业务规则?所以,通常我们都需要把领域模型和持久化模型分离。

NoSQL是最适合的聚合持久化方案吗?

但是,有没有例外呢?有,因此我们有了第2个问题:NoSQL是最适合的聚合持久化方案吗?我们知道NoSQL数据库以文档的方式保存数据,而聚合就可以作为一个文档,它天然就满足了聚合作为一个数据修改的单元,并且在一个事务中完成持久化的需求。我们根本不需要持久化对象,聚合本身就可以做持久化,看起来非常完美。

但是,通常NoSQL不支持ACID,不支持多文档的事务。例如当你有一个聚合要保存,并且同时还要保存领域事件时,他们作为不同的文档分别保存,你如何将它们放到同一个事物中当中?你可能说MongoDB已经支持ACID了,可以让这两个文档在同一个事物中,但是它毕竟才推出不久,而且性能怎么办?所以你在选型时,肯定会谨慎一些。另外,技术选型是个综合权衡的过程,要考虑多方面的因素。你肯定不会仅仅因为它更容易做聚合的持久化而选择NoSQL。所以我们还是要寻找一个更通用的解决方案。

问题的本质

让我们回到问题的本源。以订单支付这个场景为例,如下图所示。订单有订单头,有订单行,都保存在数据库中,它们构成了订单聚合。当我要做订单支付业务时,首先我们通过Repository,从数据库中得到聚合,传给Service。Service调用order.pay()方法完成业务逻辑的处理,这时候订单的状态发生了变化。然后再由Repository将变化后的聚合保存到数据库中。

但是这时候我们只有聚合的最新状态,不知道聚合发生了什么变化。因为我们前面提到分离领域模型和持久化模型。所以这时候没有Hibernate之类的框架帮我们维护状态。聚合之前的状态在哪里?在数据库里。如果从数据库再查询一遍,不仅有性能损耗,而且代码也不好看。所以现在的问题变成了,我们怎么得到聚合的原始状态,这样我们才能够去做对比,从而更高效地修改数据。我们可以参照Hibernate搞个一级缓存吗。这又会引入另一个复杂度。我们不想为了解决一个问题引入另一个问题。有没有轻量级的解决办法呢?

我们设想一下,如果我们从Repository返回的不是聚合,而是一个聚合的容器。在这个容器中,不仅有聚合,还有聚合的历史快照,是不是就解决这个问题了。当你把聚合放到容器中时,它会自动创建一个快照。因此保存的时候,我们就能够通过对比快照知道聚合发生了什么变化。

我们来看下相应的代码。Repository接口稍稍做一点调整,它的返回类型和参数变成了Aggregate泛型。

public interface OrderRepository {Aggregate<Order> findById(String orderId);void save(Aggregate<Order> orderAggregate);void remove(Aggregate<Order> orderAggregate);
}

我们再看Service代码示例,先从Repository拿到Aggregate,再取出聚合根Order,调用order的checkout方法完成业务处理,最后调用Repository保存Aggregate.

public class OrderService {public void checkout(String orderId, CheckoutRequest request) {Aggregate<Order> aggregate = orderRepository.findById(orderId);Order order = aggregate.getRoot();Payment payment = new Payment(request.getAmount());order.checkout(payment);orderRepository.save(aggregate);}
}

整个方案本质上通过引入一个快照,从而可以对比数据的变化,然后做相应的数据库修改操作。虽然这个方案修改了Repository的接口定义,但是影响范围是有限的,因为这个接口只在Service和Repository之间使用。所以影响范围有限,方案是可行的。

聚合持久化工具:Aggregate-Persistence

Aggregate-Persistence 正是基于此方案开发的持久化工具,它非常的轻量,本身不做数据库操作,它做的事情就是帮助你构建这个聚合容器,你可以用它来跟踪状态的变化:

  • 识别出哪些属性发生了变化,这样你就可以很容易只修改发生变化的数据库字段,而不是所有字段;
  • 识别出实体集合的变化,例如它能帮你识别出订单聚合中的订单行的增加、修改和删除,因此你做相应的数据库修改操作。

同时,它提供了基于Version的乐观锁支持,确保聚合作为一个工作单元整体被持久化。订单聚合持久化示例 展示了基于Aggregate-Persistence,使用Mybatis实现订单持久化的例子。

该工具经过1年多的不断完善,已经日渐成熟,日前发布了1.2版本,更多详情请访问Aggregate-Persistence。

事物与持久化_DDD之聚合持久化应该怎么做?相关推荐

  1. android 本地数据库持久化框架,android数据库持久化框架, ormlite框架,

    前言 Android中内置了SQLite,但是对于数据库操作这块,非常的麻烦.其实可以试用第3方的数据库持久化框架对之进行结构上调整, 摆脱了访问数据库操作的细节,不用再去写复杂的SQL语句.虽然这样 ...

  2. luoguP5055 【模板】可持久化文艺平衡树 可持久化非旋转treap

    luoguP5055 [模板]可持久化文艺平衡树 可持久化非旋转treap 好题. Code: #include<bits/stdc++.h> using namespace std; # ...

  3. 【Redis持久化】:Redis持久化的意义

    文章目录 1. Redis持久化的意义 2. Redis持久化方式 1. Redis持久化的意义 Redis作为分布式缓存架构中重要的一环,用于保存一些较为重要的数据,抗住系统的高并发访问.因此Red ...

  4. 【模板】可持久化数组(可持久化线段树/平衡树)

    题目背景 UPDATE : 最后一个点时间空间已经放大 标题即题意 有了可持久化数组,便可以实现很多衍生的可持久化功能(例如:可持久化并查集) 题目描述 如题,你需要维护这样的一个长度为 N N N ...

  5. nacos持久化配置MySQL8_Nacos 数据持久化 mysql8.0

    java java8 java开发 Nacos 数据持久化 mysql8.0 一.问题描述 直接下载的稳定版本nacos编译后的文件,不支持mysql8及其以上版本,按照官网文档:https://na ...

  6. P3919 【模板】可持久化数组(可持久化线段树/平衡树)

    题目描述 如题,你需要维护这样的一个长度为 N  的数组,支持如下几种操作 在某个历史版本上修改某一个位置上的值 访问某个历史版本上的某一位置的值 此外,每进行一次操作(对于操作2,即为生成一个完全一 ...

  7. vuex状态持久化_Vuex数据状态持久化

    vuex可以进行全局的状态管理,但刷新后刷新后数据会消失,这是我们不愿意看到的.怎么解决呢,我们可以结合本地存储做到数据状态持久化,也可以通过插件vuex-persistedstate. 1.手动利用 ...

  8. LGOJ P3919【模板】可持久化数组(可持久化线段树/平衡树)

    代码 //可持久化线段树 #include <cstdio> using namespace std;struct node {node *Lnode,*Rnode;int val;voi ...

  9. php使用redis持久化,redis是如何持久化的

    Redis是一种高级key-value数据库.它跟memcached类似,不过数据可以持久化,而且支持的数据类型很丰富. 有字符串,链表,集 合和有序集合.支持在服务器端计算集合的并,交和补集(dif ...

最新文章

  1. oracle em 乱码问题
  2. 赠书:全球首本VS Code中文书来了,高效编程秘诀全收录!
  3. 主mysql删除从服务不同步_MySQL主从不同步解决
  4. html标签书写注意事项,HTML一些标签注意事项
  5. mysql8支持myISAM_mysql菜鸟手迹8--mysql存储引擎之MyISAM
  6. 2021年中国人力资源服务行业研究报告
  7. 2016,我似乎明白了
  8. OpenCV+QT5在Window下的环境配置记录
  9. java聊天室设计_JAVA网络聊天室的设计与实现
  10. Betwin实现电脑一分为二
  11. IPv4与IPv6的区别
  12. 本地文件上传到github
  13. 探索开源工作流引擎Azkaban在MRS中的实践
  14. 编程小技巧之 Linux 文本处理命令(二)
  15. 4K智能AI双目自动跟踪云台摄像机
  16. 小学生计算机校本课程教材,二年级校本课程教材
  17. amis eureka_如何构建量身定制的amis来升级您的基础架构
  18. 【Java】 杨辉三角 二维数组打印杨辉三角
  19. 服务器共享文件搜索慢的原因,客户端访问服务器共享文件反应很慢.doc
  20. python图像预处理-(二)腐蚀与膨胀

热门文章

  1. 百度邀您来听一场云存储的大戏!
  2. 专访 Unity 技术总监 Mark Schoennagel、杨栋:基于 Unity 制作影视动画的几点建议!
  3. “腾讯也感受到危机了!”
  4. Github 用户喊话微软:放弃 ICE 吧,不然会失去我们的
  5. 万万没想到,区块链将提升无人驾驶安全性!
  6. 字节跳动算法工程师总结:java自学路线及推荐书籍
  7. mysql查出的结果太大 php_如何解决PHP使用mysql_query查询超大结果集超内存问题
  8. 二分查找(java)
  9. 点击文本框内容消失,移开内容自动显示(两种方法)(原创)
  10. oracle狎鸥亭_卢永佑 个人主页 - 韩国奥拉克皮肤科整外科 - 美佳网