private OrderDAO orderDAO;
public Long addOrder(RequestDTO request) {
// 此处省略很多拼装逻辑
OrderDO orderDO = new OrderDO();
orderDAO.insertOrder(orderDO);
return orderDO.getId();
}
public void updateOrder(OrderDO orderDO, RequestDTO updateRequest) {
orderDO.setXXX(XXX); // 省略很多
orderDAO.updateOrder(orderDO);
}
public void doSomeBusiness(Long id) {
OrderDO orderDO = orderDAO.getOrderById(id);
// 此处省略很多业务逻辑
}
private OrderDAO orderDAO;
private Cache cache;
public Long addOrder(RequestDTO request) {
// 此处省略很多拼装逻辑
OrderDO orderDO = new OrderDO();
orderDAO.insertOrder(orderDO);
cache.put(orderDO.getId(), orderDO);
return orderDO.getId();
}
public void updateOrder(OrderDO orderDO, RequestDTO updateRequest) {
orderDO.setXXX(XXX); // 省略很多
orderDAO.updateOrder(orderDO);
cache.put(orderDO.getId(), orderDO);
}
public void doSomeBusiness(Long id) {
OrderDO orderDO = cache.get(id);
if (orderDO == null) {
orderDO = orderDAO.getOrderById(id);
}
// 此处省略很多业务逻辑
}
这时,你会发现因为插入的逻辑变化了,导致在所有的使用数据的地方,都需要从1行代码改为至少3行。而当你的代码量变得比较大,然后如果在某个地方你忘记了查缓存,或者在某个地方忘记了更新缓存,轻则需要查数据库,重则是缓存和数据库不一致,导致bug。当你的代码量变得越来越多,直接调用DAO、缓存的地方越来越多时,每次底层变更都会变得越来越难,越来越容易导致bug。这就是软件被“固化”的后果。
public class DtoAssembler {
// 通过各种实体,生成DTO
public OrderDTO toDTO(Order order, Item item) {
OrderDTO dto = new OrderDTO();
dto.setId(order.getId());
dto.setItemTitle(item.getTitle()); // 从多个对象里取值,且字段名称不一样
dto.setDetailAddress(order.getAddress.getDetail()); // 可以读取复杂嵌套字段
// 省略N行
return dto;
}
// 通过DTO,生成实体
public Item toEntity(ItemDTO itemDTO) {
Item entity = new Item();
entity.setId(itemDTO.getId());
// 省略N行
return entity;
}
}
我们能看出来通过抽象出一个Assembler/Converter对象,我们能把复杂的转化逻辑都收敛到一个对象中,并且可以很好的单元测试。这个也很好的收敛了常见代码里的转化逻辑。
public class Application {
private DtoAssembler assembler;
private OrderRepository orderRepository;
private ItemRepository itemRepository;
public OrderDTO getOrderDetail(Long orderId) {
Order order = orderRepository.find(orderId);
Item item = itemRepository.find(order.getItemId());
return assembler.toDTO(order, item); // 原来的很多复杂转化逻辑都收敛到一行代码了
}
}
@org.mapstruct.Mapper
public interface DtoAssembler { // 注意这里变成了一个接口,MapStruct会生成实现类
DtoAssembler INSTANCE = Mappers.getMapper(DtoAssembler.class);
// 在这里只需要指出字段不一致的情况,支持复杂嵌套
@Mapping(target = "itemTitle", source = "item.title")
@Mapping(target = "detailAddress", source = "order.address.detail")
OrderDTO toDTO(Order order, Item item);
// 如果字段没有不一致,一行注解都不需要
Item toEntity(ItemDTO itemDTO);
}
在使用了MapStruct后,你只需要标注出字段不一致的情况,其他的情况都通过Convention over Configuration帮你解决了。还有很多复杂的用法我就不一一指出了。
DO | Entity | DTO | |
目的 | 数据库表映射 | 业务逻辑 | 适配业务场景 |
代码层级 | Infrastructure | Domain | Application |
命名规范 | XxxDO | Xxx | XxxDTO XxxCommand XxxRequest等 |
字段名称标准 | 数据库表字段名 | 业务语言 | 和调用方商定 |
字段数据类型 | 数据库字段类型 | 尽量是有业务含义的类型,比如DP | 和调用方商定 |
是否需要序列化 | 不需要 | 不需要 | 需要 |
转化器 | Data Converter | Data Converter DTO Assembler | DTO Assembler |
从使用复杂度角度来看,区分了DO、Entity、DTO带来了代码量的膨胀(从1个变成了3+2+N个)。但是在实际复杂业务场景下,通过功能来区分模型带来的价值是功能性的单一和可测试、可预期,最终反而是逻辑复杂性的降低。
上文曾经讲过,传统Data Mapper(DAO)属于“固件”,和底层实现(DB、Cache、文件系统等)强绑定,如果直接使用会导致代码“固化”。所以为了在Repository的设计上体现出“软件”的特性,主要需要注意以下三点:
我们先定义一个基础的 Repository 基础接口类,以及一些Marker接口类:
public interface Repository<T extends Aggregate<ID>, ID extends Identifier> {
/**
* 将一个Aggregate附属到一个Repository,让它变为可追踪。
* Change-Tracking在下文会讲,非必须
*/
void attach(@NotNull T aggregate);
/**
* 解除一个Aggregate的追踪
* Change-Tracking在下文会讲,非必须
*/
void detach(@NotNull T aggregate);
/**
* 通过ID寻找Aggregate。
* 找到的Aggregate自动是可追踪的
*/
T find(@NotNull ID id);
/**
* 将一个Aggregate从Repository移除
* 操作后的aggregate对象自动取消追踪
*/
void remove(@NotNull T aggregate);
/**
* 保存一个Aggregate
* 保存后自动重置追踪条件
*/
void save(@NotNull T aggregate);
}
// 聚合根的Marker接口
public interface Aggregate<ID extends Identifier> extends Entity<ID> {
}
// 实体类的Marker接口
public interface Entity<ID extends Identifier> extends Identifiable<ID> {
}
public interface Identifiable<ID extends Identifier> {
ID getId();
}
// ID类型DP的Marker接口
public interface Identifier extends Serializable {
}
业务自己的接口只需要在基础接口上进行扩展,举个订单的例子:
// 代码在Domain层
public interface OrderRepository extends Repository<Order, OrderId> {
// 自定义Count接口,在这里OrderQuery是一个自定义的DTO
Long count(OrderQuery query);
// 自定义分页查询接口
Page<Order> query(OrderQuery query);
// 自定义有多个条件的查询接口
Order findInStore(OrderId id, StoreId storeId);
}
每个业务需要根据自己的业务场景来定义各种查询逻辑。
这里需要再次强调的是Repository的接口是在Domain层,但是实现类是在Infrastructure层。
// 代码在Infrastructure层
@Repository // Spring的注解
public class OrderRepositoryImpl implements OrderRepository {
private final OrderDAO dao; // 具体的DAO接口
private final OrderDataConverter converter; // 转化器
public OrderRepositoryImpl(OrderDAO dao) {
this.dao = dao;
this.converter = OrderDataConverter.INSTANCE;
}
@Override
public Order find(OrderId orderId) {
OrderDO orderDO = dao.findById(orderId.getValue());
return converter.fromData(orderDO);
}
@Override
public void remove(Order aggregate) {
OrderDO orderDO = converter.toData(aggregate);
dao.delete(orderDO);
}
@Override
public void save(Order aggregate) {
if (aggregate.getId() != null && aggregate.getId().getValue() > 0) {
// update
OrderDO orderDO = converter.toData(aggregate);
dao.update(orderDO);
} else {
// insert
OrderDO orderDO = converter.toData(aggregate);
dao.insert(orderDO);
aggregate.setId(converter.fromData(orderDO).getId());
}
}
@Override
public Page<Order> query(OrderQuery query) {
List<OrderDO> orderDOS = dao.queryPaged(query);
long count = dao.count(query);
List<Order> result = orderDOS.stream().map(converter::fromData).collect(Collectors.toList());
return Page.with(result, query, count);
}
@Override
public Order findInStore(OrderId id, StoreId storeId) {
OrderDO orderDO = dao.findInStore(id.getValue(), storeId.getValue());
return converter.fromData(orderDO);
}
}
public class OrderRepositoryImpl extends implements OrderRepository {
private OrderDAO orderDAO;
private LineItemDAO lineItemDAO;
private OrderDataConverter orderConverter;
private LineItemDataConverter lineItemConverter;
// 其他逻辑省略
@Override
public void save(Order aggregate) {
if (aggregate.getId() != null && aggregate.getId().getValue() > 0) {
// 每次都将Order和所有LineItem全量更新
OrderDO orderDO = orderConverter.toData(aggregate);
orderDAO.update(orderDO);
for (LineItem lineItem: aggregate.getLineItems()) {
save(lineItem);
}
} else {
// 插入逻辑省略
}
}
private void save(LineItem lineItem) {
if (lineItem.getId() != null && lineItem.getId().getValue() > 0) {
LineItemDO lineItemDO = lineItemConverter.toData(lineItem);
lineItemDAO.update(lineItemDO);
} else {
LineItemDO lineItemDO = lineItemConverter.toData(lineItem);
lineItemDAO.insert(lineItemDO);
lineItem.setId(lineItemConverter.fromData(lineItemDO).getId());
}
}
}
// 这个类是一个通用的支撑类,为了减少开发者的重复劳动。在用的时候需要继承这个类
public abstract class DbRepositorySupport<T extends Aggregate<ID>, ID extends Identifier> implements Repository<T, ID> {
@Getter
private final Class<T> targetClass;
// 让AggregateManager去维护Snapshot
@Getter(AccessLevel.PROTECTED)
private AggregateManager<T, ID> aggregateManager;
protected DbRepositorySupport(Class<T> targetClass) {
this.targetClass = targetClass;
this.aggregateManager = AggregateManager.newInstance(targetClass);
}
/**
* 这几个方法是继承的子类应该去实现的
*/
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);
/**
* Attach的操作就是让Aggregate可以被追踪
*/
@Override
public void attach(@NotNull T aggregate) {
this.aggregateManager.attach(aggregate);
}
/**
* Detach的操作就是让Aggregate停止追踪
*/
@Override
public void detach(@NotNull T aggregate) {
this.aggregateManager.detach(aggregate);
}
@Override
public T find(@NotNull ID id) {
T aggregate = this.onSelect(id);
if (aggregate != null) {
// 这里的就是让查询出来的对象能够被追踪。
// 如果自己实现了一个定制查询接口,要记得单独调用attach。
this.attach(aggregate);
}
return aggregate;
}
@Override
public void remove(@NotNull T aggregate) {
this.onDelete(aggregate);
// 删除停止追踪
this.detach(aggregate);
}
@Override
public void save(@NotNull T aggregate) {
// 如果没有ID,直接插入
if (aggregate.getId() == null) {
this.onInsert(aggregate);
this.attach(aggregate);
return;
}
// 做Diff
EntityDiff diff = aggregateManager.detectChanges(aggregate);
if (diff.isEmpty()) {
return;
}
// 调用UPDATE
this.onUpdate(aggregate, diff);
// 最终将DB带来的变化更新回AggregateManager
aggregateManager.merge(aggregate);
}
}
public class OrderRepositoryImpl extends DbRepositorySupport<Order, OrderId> implements OrderRepository {
private OrderDAO orderDAO;
private LineItemDAO lineItemDAO;
private OrderDataConverter orderConverter;
private LineItemDataConverter lineItemConverter;
// 部分代码省略,见上文
@Override
protected void onUpdate(Order aggregate, EntityDiff diff) {
if (diff.isSelfModified()) {
OrderDO orderDO = converter.toData(aggregate);
orderDAO.update(orderDO);
}
Diff lineItemDiffs = diff.getDiff("lineItems");
if (lineItemDiffs instanceof ListDiff) {
ListDiff diffList = (ListDiff) lineItemDiffs;
for (Diff itemDiff : diffList) {
if (itemDiff.getType() == DiffType.Removed) {
LineItem line = (LineItem) itemDiff.getOldValue();
LineItemDO lineDO = lineItemConverter.toData(line);
lineItemDAO.delete(lineDO);
}
if (itemDiff.getType() == DiffType.Added) {
LineItem line = (LineItem) itemDiff.getNewValue();
LineItemDO lineDO = lineItemConverter.toData(line);
lineItemDAO.insert(lineDO);
}
if (itemDiff.getType() == DiffType.Modified) {
LineItem line = (LineItem) itemDiff.getNewValue();
LineItemDO lineDO = lineItemConverter.toData(line);
lineItemDAO.update(lineDO);
}
}
}
}
}
class ThreadLocalAggregateManager<T extends Aggregate<ID>, ID extends Identifier> implements AggregateManager<T, ID> {
private ThreadLocal<DbContext<T, ID>> context;
private Class<? extends T> targetClass;
public ThreadLocalAggregateManager(Class<? extends T> targetClass) {
this.targetClass = targetClass;
this.context = ThreadLocal.withInitial(() -> new DbContext<>(targetClass));
}
public void attach(T aggregate) {
context.get().attach(aggregate);
}
@Override
public void attach(T aggregate, ID id) {
context.get().setId(aggregate, id);
context.get().attach(aggregate);
}
@Override
public void detach(T aggregate) {
context.get().detach(aggregate);
}
@Override
public T find(ID id) {
return context.get().find(id);
}
@Override
public EntityDiff detectChanges(T aggregate) {
return context.get().detectChanges(aggregate);
}
public void merge(T aggregate) {
context.get().merge(aggregate);
}
}
class DbContext<T extends Aggregate<ID>, ID extends Identifier> {
@Getter
private Class<? extends T> aggregateClass;
private Map<ID, T> aggregateMap = new HashMap<>();
public DbContext(Class<? extends T> aggregateClass) {
this.aggregateClass = aggregateClass;
}
public void attach(T aggregate) {
if (aggregate.getId() != null) {
if (!aggregateMap.containsKey(aggregate.getId())) {
this.merge(aggregate);
}
}
}
public void detach(T aggregate) {
if (aggregate.getId() != null) {
aggregateMap.remove(aggregate.getId());
}
}
public EntityDiff detectChanges(T aggregate) {
if (aggregate.getId() == null) {
return EntityDiff.EMPTY;
}
T snapshot = aggregateMap.get(aggregate.getId());
if (snapshot == null) {
attach(aggregate);
}
return DiffUtils.diff(snapshot, aggregate);
}
public T find(ID id) {
return aggregateMap.get(id);
}
public void merge(T aggregate) {
if (aggregate.getId() != null) {
T snapshot = SnapshotUtils.snapshot(aggregate);
aggregateMap.put(aggregate.getId(), snapshot);
}
}
public void setId(T aggregate, ID id) {
ReflectionUtils.writeField("id", aggregate, id);
}
}
@Test
public void multiInsert() {
OrderDAO dao = new MockOrderDAO();
OrderRepository repo = new OrderRepositoryImpl(dao);
Order order = new Order();
order.setUserId(new UserId(11L));
order.setStatus(OrderState.ENABLED);
order.addLineItem(new ItemId(13L), new Quantity(5), new Money(4));
order.addLineItem(new ItemId(14L), new Quantity(2), new Money(3));
System.out.println("第一次保存前");
System.out.println(order);
repo.save(order);
System.out.println("第一次保存后");
System.out.println(order);
order.getLineItems().get(0).setQuantity(new Quantity(3));
order.pay();
repo.save(order);
System.out.println("第二次保存后");
System.out.println(order);
}
第一次保存前
Order(id=null, userId=11, lineItems=[LineItem(id=null, itemId=13, quantity=5, price=4), LineItem(id=null, itemId=14, quantity=2, price=3)], status=ENABLED)
INSERT OrderDO: OrderDO(id=null, parentId=null, itemId=0, userId=11, quantity=0, price=0, status=2)
UPDATE OrderDO: OrderDO(id=1001, parentId=1001, itemId=0, userId=11, quantity=0, price=0, status=2)
INSERT OrderDO: OrderDO(id=null, parentId=1001, itemId=13, userId=11, quantity=5, price=4, status=2)
INSERT OrderDO: OrderDO(id=null, parentId=1001, itemId=14, userId=11, quantity=2, price=3, status=2)
第一次保存后
Order(id=1001, userId=11, lineItems=[LineItem(id=1002, itemId=13, quantity=5, price=4), LineItem(id=1003, itemId=14, quantity=2, price=3)], status=ENABLED)
UPDATE OrderDO: OrderDO(id=1001, parentId=1001, itemId=0, userId=11, quantity=0, price=0, status=3)
UPDATE OrderDO: OrderDO(id=1002, parentId=1001, itemId=13, userId=11, quantity=3, price=4, status=3)
第二次保存后
Order(id=1001, userId=11, lineItems=[LineItem(id=1002, itemId=13, quantity=3, price=4), LineItem(id=1003, itemId=14, quantity=2, price=3)], status=PAID)
▐ 其他注意事项
END