领域驱动(DDD:Domain-Driven Design)在业界已经流行多年,经验丰富的程序员或多或少都在项目中引入了一些DDD的思想,但完全遵照DDD构建的项目却很少。除了领会DDD思想有一定难度外,面向对象与数据库实体模型间的阻抗也是一个非常重要的原因,这个原因也一直困扰我很长时间。
文本中以日常熟悉的订单为例,讨论一下使用DDD会遇到哪些问题以及如何解决。订单聚合包括订单(Order)、订单明细行(OrderItem)两个实体,其中订单是聚合根。很多讲述DDD的文章中经常以类似的代码进行讲解,本文中我们也延续这种描述方式。
publicclassOrder{ /** * 订单聚合 */ privateLong id; privateCustomer customer; privateOrderStatus status; privateBigDecimal totalPrice; privateBigDecimal totalPayment; // 其他属性 /** * 订单项子聚合 */ privateList<OrderItem> orderItems; /** * 创建聚合根 */ publicstaticOrdercreate(/*输入参数*/){ List<OrderItem> items =newArrayList<>(); items.add(/**/); items.add(/**/); Order order =newOrder(); order.setItems(items); order.setStatus(/**/); // ... return order; } } public class OrderItem { private Long id; private Product product; private BigDecimal amount; private BigDecimal subTotal; private OrderItemStatus status; // 其他属性 } @Service public class OrderServiceImpl implements OrderService { @Autowire private OrderRepository orderRepository; @Override @Transcation public void createOrder(OrderCommand command) { Order order = Order.create(/*输入参数*/); orderRepository.save(order); } }
到目前为止,代码看起来很干净、漂亮,完全符合DDD设计,Order是一个聚合根,OrderItem是其中的子聚合,但我们并没有展示OrderRepository中的代码,事实上DDD实现层面最难处理的就是Repository。通常有这么几个难点:
-
Repository中的save方法如何实现upsert处理。
-
对于1-N关系,如何判断具体哪个元素发生变更(对应数据库中的增加、修改、删除)。
-
对于1-N关系,如何处理N过大的问题。
我们先来看一下
问题一
如何实现upsert逻辑。众所周知,关系型数据库将插入、更新分为两个独立操作。代码层面我们希望save能够实现upsert逻辑,代码中可以通过order.id是否为null进行区分,如果id等于null意味着是一个新的对象需要执行insert,否则执行update。
@Repository publicclassOrderRepositoryImplimplementsOrderRepository{ @Autowire privateOrderMapper orderMapper; @Autowire privateOrderItemMapper orderItemMapper; @Override publicvoidsave(Order order){ if(order.getId() ==null) { orderMapper.insert(order); for(OrderItem item : order.getItems()) { orderItemMapper.insert(item) } }else{ // update } } }
上面代码中并没有给出update的实现过程,通常在service中会这样写代码。
@Service publicclassOrderServiceImplimplementsOrderService{ @Autowire privateOrderRepository orderRepository; @Override @Transcation publicvoidupdateOrder(/*输入参数*/){ Order order = orderRepository.find(orderId); order.getOrderItems().get(orderItemNumber).setStatus(/**/); orderRepository.save(order); } }
前文中提到Order与OrderItem是1对N的关系,Order聚合根包含着order表中的一行数据和order_item表中的N行数据,对聚合根的操作放在service中,而实际的db更新却在OrderRepository.save中。
对于问题二
,在1-N关系中判断哪个元素发生变更就是一个要解决的问题。
代码中,可以在每个实体上添加一个字段记录变更状态来解决这个问题。
publicclassEntityState{ /** * 记录变化状态,1:insert、2:update、3:delete、4: none */ protectedintchangeState; } publicclassOrderextendEntityState{ privateLong id; privateCustomer customer; privateOrderStatus status; privateBigDecimal totalPrice; privateBigDecimal totalPayment; privateList<OrderItem> items; } publicclassOrderItemextendEntityState{ privateLong id; privateProduct product; privateBigDecimal amount; privateBigDecimal subTotal; privateOrderStatus status; // 其他属性 publicvoidsetStatus(OrderStatus status){ // update this.changeState =2; this.status = status; // ... } // ... } @Repository publicclassOrderRepositoryImplimplementsOrderRepository{ @Autowire privateOrderMapper orderMapper; @Autowire privateOrderItemMapper orderItemMapper; @Override publicvoidsave(Order order){ if(order.getId() ==null) { // insert ... // ... }else{ // 处理Order // ... // 处理OrderItem for(OrderItem item : order.getOrderItems()) { switch(item.getChangeStatus()) { case1: orderItemMapper.insert(item); break; case2: orderItemMapper.update(item); break; case3: orderItemMapper.delete(item.getId()); break; default: break; } } } } } @Service publicclassOrderServiceImplimplementsOrderService{ @Autowire privateOrderRepository orderRepository; @Override @Transcation publicvoidupdateOrder(OrderItem orderItem){ Order order = orderRepository.find(orderId); OrderItem orderItem = order.getOrderItems().stream().filter(elem -> elem.getId().equals(orderItem.getId())).findFirst().get(); orderItem.setStatus(/**/); orderRepository.save(order); } }
解决完更新的问题,我们再来看看查询的场景。
publicclassOrder{ privateLong id; privateCustomer customer; privateOrderStatus status; privateBigDecimal totalPrice; privateBigDecimal totalPayment; privateList<OrderItem> orderItems; // 其他属性 } publicclassOrderItem{ privateLong id; privateProduct product; privateBigDecimal amount; privateBigDecimal subTotal; privateOrderStatus status; // 其他属性 } @Repository publicclassOrderRepositoryImplimplementsOrderRepository{ @Autowire privateOrderMapper orderMapper; @Autowire privateOrderItemMapper orderItemMapper; @Override publicOrderfind(longid){ returnnewOrder(orderMapper.select(/**/), orderItemMapper.select(/**/)); } }
上面代码,Order中包含一个List<OrderItem>,在OrderRepository.find中进行两次数据库查询完成Order聚合根组装。如果OrderItem数量较少这没什么问题,但对于数据量较大的场景显然不能将OrderItem一次性查出全部放入内存。这就引出了
问题三:“对于1-N关系,如何处理N过大的问题”。
一种变通的方法是Order不存储List<OrderItem> orderItems,只存储OrderItems的变更,这时候充血模型变成了失血模型。
publicclassOrder{ privateLong id; privateCustomer customer; privateOrderStatus status; privateBigDecimal totalPrice; privateBigDecimal totalPayment; privateList<OrderItem> changeOrderItems; /** * 直接通过SQL查询数据 */ publicList<OrderItem>getOrders(/*查询条件*/){ // select * from order_item where ... returnorderItems; } publicvoidaddOrderItem(OrderItem orderItem){ // 新增 orderItem.setChangeState(1); changeOrderItems.add(orderItem); } publicvoidupdateOrderItem(OrderItem orderItem){ // 更新 orderItem.setChangeState(2); changeOrderItems.add(orderItem); } publicvoidremoveOrderItem(OrderItem orderItem){ // 删除 orderItem.setChangeState(3); changeOrderItems.add(orderItem); } } @Repository publicclassOrderRepositoryImplimplementsOrderRepository{ @Autowire privateOrderMapper orderMapper; @Autowire privateOrderItemMapper orderItemMapper; @Override publicvoidsave(Order order){ // ... // 处理OrderItem变更 for(OrderItem item : order.getChangeStatus()) { switch(item.getChangeStatus()) { case1: orderItemMapper.insert(item); break; case2: orderItemMapper.update(item); break; case3: orderItemMapper.delete(item.getId()); break; default: break; } } } } }
对于1-N问题,《实现领域驱动设计》也给出了相应的方案
有时,如果我们要获取聚合根下的某些子聚合,我们不用先从资源库中获取到聚合根,然后再从聚合根中获取这些子聚合,而是可以直接从资源库中返回。在有些情况下,这种做法是有好处的。比如,某个聚合根拥有一个很大的实体类型集合,而你需要根据某种查询条件返回该集合中的一部分实体。当然,只有在聚合根中提供了对该实体集合的导航时,我们才能这么做,否则,我们便违背了聚合的设计原则。我建议不要因为客户端的方便而提供这种访问方式。更多的时候,采用这种方式是由于性能上的考虑,比如从聚合根中访问子聚合将带来性能瓶颈的时候。此时的查找方法和其他查找方法具有相同的基本特征,只是它直接返回聚合根下的子聚合,而不是聚合根本身。无论如何,请慎重使用这种方式。
除了这些问题外,应用DDD也还有其他问题:
-
Repository无法实现批量操作,比如直接delete from order_item where id = :v1 or id = :v2
-
查询性能低,如果想操作order_item表,需要通过Repository.find查处理order表中的数据,然后才关联查询出order_item表中的数据。