DDD实战 - Repository模式的妙用( 四 )


变更追踪有两种主流实现方式:

  1. 基于快照Snapshot的方案: 数据从数据库提取后,在内存中保存一份快照,然后在将数据写回时与快照进行比较 。Hibernate是采用此种方法的常见实现 。
  2. 基于代理Proxy的方案: 当数据从数据库提取后,通过织入的方式为所有setter方法增加一个切面来检测setter是否被调用以及值是否发生变化 。如果值发生变化,则将其标记为“脏”(Dirty) 。在保存时,根据这个标记来判断是否需要更新 。Entity Framework是一个采用此种方法的常见实现 。
代理Proxy方案的优势是性能较高,几乎没有额外成本,但缺点是实现起来比较复杂,而且当存在嵌套关系时,不容易检测到嵌套对象的变化(例如,子列表的增加和删除),可能会导致bug 。
而快照Snapshot方案的优势是实现相对简单,成本在于每次保存时执行全量比较(通常使用反射)以及保存快照的内存消耗 。
由于代理Proxy方案的复杂性,业界主流(包括EF Core)更倾向于使用基于Snapshot快照的方案 。
此外,通过检测差异,我们能识别哪些字段发生了改变,并仅更新这些发生变化的字段,从而进一步降低UPDATE操作的开销 。无论是否在DDD上下文中,这个功能本身都是非常有用的 。在DailyMart示例中,我们使用一个名为DiffUtils的工具类来辅助比较对象间的差异 。
public class DiffUtilsTest {@Testpublic void diffObject() throws IllegalAccessException, IOException, ClassNotFoundException {//实时对象Order realObj = Order.builder().id(new OrderId(31L)).customerId(100L).totalAmount(new BigDecimal(100)).recipientInfo(new RecipientInfo("zhangsan","安徽省合肥市","123456")).build(); // 快照对象Order snapshotObj = SnapshotUtils.snapshot(realObj);snapshotObj.setId(new OrderId(2L));snapshotObj.setTotalAmount(new BigDecimal(200));EntityDiff diff = DiffUtils.diff(realObj, snapshotObj);assertTrue(diff.isSelfModified());assertEquals(2, diff.getDiffs().size());}}详细用法可以参考单元测试com.jianzh5.dailymart.module.order.infrastructure.util.DiffUtilsTest
通过变更追踪的引入,我们能够使聚合的Repository实现更加高效和智能 。这允许开发人员将注意力集中在业务逻辑上,而不必担心不必要的数据库操作 。
DDD实战 - Repository模式的妙用

文章插图
图片
DDD实战 - Repository模式的妙用

文章插图
图片
5 在DailyMart中集成变更追踪DailyMart系统内涵盖了一个订单子域,该子域以Order作为聚合根,并将OrderItem纳入为其子实体 。两者之间构成一对多的联系 。在对订单进行更新操作时,变更追踪显得尤为关键 。
下面展示的是DailyMart系统中关于变更追踪的核心代码片段 。值得注意的是,这些代码仅用于展示如何在仓库模式中融入变更追踪,并非订单子域的完整实现 。
AggregateRepositorySupport 类该类是聚合仓库的支持类,它管理聚合的变更追踪 。
@Slf4jpublic abstract class AggregateRepositorySupport<T extends Aggregate<ID>, ID extends Identifier<?>>implements Repository<T, ID> {@Getterprivate final Class<T> targetClass;// 让 AggregateManager 去维护 Snapshot@Getter(AccessLevel.PROTECTED)private AggregateManager<T, ID> aggregateManager;protected AggregateRepositorySupport(Class<T> targetClass) {this.targetClass = targetClass;this.aggregateManager = AggregateManagerFactory.newInstance(targetClass);}/** Attach的操作就是让Aggregate可以被追踪 */@Overridepublic void attach(@NotNull T aggregate) {this.aggregateManager.attach(aggregate);}/** Detach的操作就是让Aggregate停止追踪 */@Overridepublic void detach(@NotNull T aggregate) {this.aggregateManager.detach(aggregate);}@Overridepublic T find(@NotNull ID id) {T aggregate = this.onSelect(id);if (aggregate != null) {// 这里的就是让查询出来的对象能够被追踪 。// 如果自己实现了一个定制查询接口,要记得单独调用attach 。this.attach(aggregate);}return aggregate;}@Overridepublic void remove(@NotNull T aggregate) {this.onDelete(aggregate);// 删除停止追踪this.detach(aggregate);}@Overridepublic void save(@NotNull T aggregate) {// 如果没有 ID,直接插入if (aggregate.getId() == null) {this.onInsert(aggregate);this.attach(aggregate);return;}// 做 DiffEntityDiff diff = null;try {//aggregate = this.onSelect(aggregate.getId());find(aggregate.getId());diff = aggregateManager.detectChanges(aggregate);} catch (IllegalAccessException e) {//throw new RuntimeException("Failed to detect changes", e);e.printStackTrace();}if (diff.isEmpty()) {return;}// 调用 UPDATEthis.onUpdate(aggregate, diff);// 最终将 DB 带来的变化更新回 AggregateManageraggregateManager.merge(aggregate);}/** 这几个方法是继承的子类应该去实现的 */protected abstract void onInsert(T aggregate);protected abstract T onSelect(ID id);protected abstract void onUpdate(T aggregate, EntityDiff diff);protected abstract void onDelete(T aggregate);}


推荐阅读