写点什么

领域驱动落地实现

用户头像
星际行者
关注
发布于: 2020 年 12 月 25 日

领域驱动(DDD:Domain-Driven Design)在业界已经流行多年,经验丰富的程序员或多或少都在项目中引入了一些DDD的思想,但完全遵照DDD构建的项目却很少。除了领会DDD思想有一定难度外,面向对象与数据库实体模型间的阻抗也是一个非常重要的原因,这个原因也一直困扰我很长时间。



文本中以日常熟悉的订单为例,讨论一下使用DDD会遇到哪些问题以及如何解决。订单聚合包括订单(Order)、订单明细行(OrderItem)两个实体,其中订单是聚合根。很多讲述DDD的文章中经常以类似的代码进行讲解,本文中我们也延续这种描述方式。



public class Order {
/**
* 订单聚合
*/
private Long id;
private Customer customer;
private OrderStatus status;
private BigDecimal totalPrice;
private BigDecimal totalPayment;
// 其他属性
/**
* 订单项子聚合
*/
private List<OrderItem> orderItems;
/**
* 创建聚合根
*/
public static Order create(/*输入参数*/) {
List<OrderItem> items = new ArrayList<>();
items.add(/**/);
items.add(/**/);
Order order = new Order();
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。通常有这么几个难点:

  1. Repository中的save方法如何实现upsert处理。

  2. 对于1-N关系,如何判断具体哪个元素发生变更(对应数据库中的增加、修改、删除)。

  3. 对于1-N关系,如何处理N过大的问题。



我们先来看一下问题一如何实现upsert逻辑。众所周知,关系型数据库将插入、更新分为两个独立操作。代码层面我们希望save能够实现upsert逻辑,代码中可以通过order.id是否为null进行区分,如果id等于null意味着是一个新的对象需要执行insert,否则执行update。



@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Autowire
private OrderMapper orderMapper;
@Autowire
private OrderItemMapper orderItemMapper;
@Override
public void save(Order order) {
if(order.getId() == null) {
orderMapper.insert(order);
for(OrderItem item : order.getItems()) {
orderItemMapper.insert(item)
}
} else {
// update
}
}
}



上面代码中并没有给出update的实现过程,通常在service中会这样写代码。



@Service
public class OrderServiceImpl implements OrderService {
@Autowire
private OrderRepository orderRepository;
@Override
@Transcation
public void updateOrder(/*输入参数*/) {
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关系中判断哪个元素发生变更就是一个要解决的问题。



代码中,可以在每个实体上添加一个字段记录变更状态来解决这个问题。



public class EntityState {
/**
* 记录变化状态,1:insert、2:update、3:delete、4: none
*/
protected int changeState;
}
public class Order extend EntityState {
private Long id;
private Customer customer;
private OrderStatus status;
private BigDecimal totalPrice;
private BigDecimal totalPayment;
private List<OrderItem> items;
}
public class OrderItem extend EntityState {
private Long id;
private Product product;
private BigDecimal amount;
private BigDecimal subTotal;
private OrderStatus status;
// 其他属性
public void setStatus(OrderStatus status) {
// update
this.changeState = 2;
this.status = status;
// ...
}
// ...
}
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Autowire
private OrderMapper orderMapper;
@Autowire
private OrderItemMapper orderItemMapper;
@Override
public void save(Order order) {
if(order.getId() == null) {
// insert ...
// ...
} else {
// 处理Order
// ...
// 处理OrderItem
for(OrderItem item : order.getOrderItems()) {
switch(item.getChangeStatus()) {
case 1:
orderItemMapper.insert(item);
break;
case 2:
orderItemMapper.update(item);
break;
case 3:
orderItemMapper.delete(item.getId());
break;
default:
break;
}
}
}
}
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowire
private OrderRepository orderRepository;
@Override
@Transcation
public void updateOrder(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);
}
}



解决完更新的问题,我们再来看看查询的场景。



public class Order {
private Long id;
private Customer customer;
private OrderStatus status;
private BigDecimal totalPrice;
private BigDecimal totalPayment;
private List<OrderItem> orderItems;
// 其他属性
}
public class OrderItem {
private Long id;
private Product product;
private BigDecimal amount;
private BigDecimal subTotal;
private OrderStatus status;
// 其他属性
}
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Autowire
private OrderMapper orderMapper;
@Autowire
private OrderItemMapper orderItemMapper;
@Override
public Order find(long id) {
return new Order(orderMapper.select(/**/), orderItemMapper.select(/**/));
}
}



上面代码,Order中包含一个List<OrderItem>,在OrderRepository.find中进行两次数据库查询完成Order聚合根组装。如果OrderItem数量较少这没什么问题,但对于数据量较大的场景显然不能将OrderItem一次性查出全部放入内存。这就引出了问题三:“对于1-N关系,如何处理N过大的问题”。



一种变通的方法是Order不存储List<OrderItem> orderItems,只存储OrderItems的变更,这时候充血模型变成了失血模型。



public class Order {
private Long id;
private Customer customer;
private OrderStatus status;
private BigDecimal totalPrice;
private BigDecimal totalPayment;
private List<OrderItem> changeOrderItems;
/**
* 直接通过SQL查询数据
*/
public List<OrderItem> getOrders(/*查询条件*/) {
// select * from order_item where ...
return orderItems;
}
public void addOrderItem(OrderItem orderItem) {
// 新增
orderItem.setChangeState(1);
changeOrderItems.add(orderItem);
}
public void updateOrderItem(OrderItem orderItem) {
// 更新
orderItem.setChangeState(2);
changeOrderItems.add(orderItem);
}
public void removeOrderItem(OrderItem orderItem) {
// 删除
orderItem.setChangeState(3);
changeOrderItems.add(orderItem);
}
}
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Autowire
private OrderMapper orderMapper;
@Autowire
private OrderItemMapper orderItemMapper;
@Override
public void save(Order order) {
// ...
// 处理OrderItem变更
for(OrderItem item : order.getChangeStatus()) {
switch(item.getChangeStatus()) {
case 1:
orderItemMapper.insert(item);
break;
case 2:
orderItemMapper.update(item);
break;
case 3:
orderItemMapper.delete(item.getId());
break;
default:
break;
}
}
}
}
}



对于1-N问题,《实现领域驱动设计》也给出了相应的方案

有时,如果我们要获取聚合根下的某些子聚合,我们不用先从资源库中获取到聚合根,然后再从聚合根中获取这些子聚合,而是可以直接从资源库中返回。在有些情况下,这种做法是有好处的。比如,某个聚合根拥有一个很大的实体类型集合,而你需要根据某种查询条件返回该集合中的一部分实体。当然,只有在聚合根中提供了对该实体集合的导航时,我们才能这么做,否则,我们便违背了聚合的设计原则。我建议不要因为客户端的方便而提供这种访问方式。更多的时候,采用这种方式是由于性能上的考虑,比如从聚合根中访问子聚合将带来性能瓶颈的时候。此时的查找方法和其他查找方法具有相同的基本特征,只是它直接返回聚合根下的子聚合,而不是聚合根本身。无论如何,请慎重使用这种方式。





除了这些问题外,应用DDD也还有其他问题:

  1. Repository无法实现批量操作,比如直接delete from order_item where id = :v1 or id = :v2

  2. 查询性能低,如果想操作order_item表,需要通过Repository.find查处理order表中的数据,然后才关联查询出order_item表中的数据。



发布于: 2020 年 12 月 25 日阅读数: 1970
用户头像

星际行者

关注

编程多年依旧热爱。。。 2019.03.28 加入

还未添加个人简介

评论 (6 条评论)

发布
用户头像
关于对象模型和关系模型之间的映射一直不简单,选对框架也很重要,国内很多使用mybatis的,就要为了这处映射写很多支持性的代码,建议使用spring data jpa试一试,基本可以做到O/R和性能折中,对于下面3个问题基本可以解决:
1、Repository中的save方法如何实现upsert处理。
2、对于1-N关系,如何判断具体哪个元素发生变更(对应数据库中的增加、修改、删除)。
3、对于1-N关系,如何处理N过大的问题。
2020 年 12 月 31 日 17:29
回复
>> 1、Repository中的save方法如何实现upsert处理。
>> 2、对于1-N关系,如何判断具体哪个元素发生变更(对应数据库中的增加、修改、删除)。
jpa等orm框架确实提供了更好的实现,使用起来很方便,本文主要探讨纯手写sql的实现,毕竟很多项目用mybatis。

>> 3、对于1-N关系,如何处理N过大的问题。
这个不管使用mybatis还是jpa都无法避免,需要特殊处理。


展开
2021 年 01 月 06 日 09:55
回复
用户头像
使用DDD做复杂查询也是一个问题,引入DDD,最好同时考虑CQRS架构一同使用。
2020 年 12 月 31 日 17:22
回复
用户头像
问题一:Order 里不应该直接包含 Customer ,而应该是 CustomerId,聚合根之间通过 ID引用
问题二:OrderItem 里包含了 OrderItemStatus,不知道这个有什么特别的用途不?一般除聚合根外,聚会内其它实体少保持状态,减少状态维护成本。(注:这种状态可以做业务上的转移设计)
2020 年 12 月 31 日 17:19
回复
用户头像
关于DDD落地实现,后续你还有打算写系列文章吗?
2020 年 12 月 31 日 17:12
回复
还有系列文章
2021 年 01 月 06 日 09:52
回复
没有更多了
领域驱动落地实现