(二)边学边用DDD-Repository镜像实现

在DDD中,由于我们操作的聚合根对象,比如我们更新用户昵称这个字段,然后在持久化的过程中,执行的是全量的持久化

vbnet 复制代码
selectById    : ==>  Preparing: SELECT id,user_name,nick_name,password,phone,email FROM cuc_customer WHERE id=?
selectById    : ==> Parameters: 1743128870491926530(String)
selectById    : <==      Total: 1
updateById    : ==>  Preparing: UPDATE cuc_customer SET user_name=?, nick_name=?, password=?, phone=?, email=? WHERE id=?
updateById    : ==> Parameters: chenzl(String), 小土豆啊(String), 123456(String), 18112341232(String), chenzl@qq.com(String), 1743128870491926530(Long)
updateById    : <==    Updates: 1

这样会带来大量无效的update语句,导致binlog暴增,给数据同步带来不必要的开销,因此仅更新改动过的字段变得非常必要。

1.设计思路

本文提供一种基于mapstruct和反射的快照(snapshot)方案来解决此类问题。

2.接口定义

  1. 为了区分领域对象和持久化对象,分别定义2个接口,由领域对象和持久化对象失败各自的接口
  2. 其中持久化对象接口除了镜像外,还定义了一个changed的get/set方法,用于标记持久化对象是否发生了变更。
java 复制代码
public interface DmoSnapshot {

    Object getSnapshot();

    void setSnapshot(Object snapshot);
}
java 复制代码
public interface PoSnapshot {

    Object getSnapshot();

    void setSnapshot(Object snapshot);

    Boolean getChanged();

    void setChanged(Boolean changed);
}

3.mapstruct实现SnapshotConverter

  1. 首先定义了部分字段是不能被清空的,即snapshot, changed, id, version
  2. 通过@AfterMapping定义方法,保证所有DO/PO互转完毕后,能执行此方法
  3. 通过判断接口是否实现,来认定是DO->PO还是PO->DO
  4. DO->PO转换时,获取DO的镜像,设置给PO,调用cleanPO方法清空PO的未修改字段
  5. PO->DO转换时,直接将PO自身作为镜像传递给DO。
  6. clearPO方法中,当镜像为空时,即非通过标准DB查询方法转换得到的DO,或者新建的领域对象,认为是修改的对象;镜像不为空时,通过反射拿到每个属性的新值和旧值,当新旧=旧值,设置属性为空,当新值!=旧值时,设置changed=true,标记这个对象需要进行持久化
java 复制代码
public interface SnapshotConverter {
    /**
     * 不清空的属性
     */
    List<String> IGNORE_FIELDS = Arrays.asList("snapshot", "changed", "id", "version");

    @AfterMapping
    default void after(Object source, @MappingTarget Object target) {
        if (source instanceof DmoSnapshot && target instanceof PoSnapshot) {
            //领域层往持久层转换
            Object snapshot = ((DmoSnapshot) source).getSnapshot();
            ((PoSnapshot) target).setSnapshot(snapshot);
            //清空PO对象上没有改变的字段,mybatis对于空值不会持久化
            cleanPO((PoSnapshot) target);
        } else if (source instanceof PoSnapshot && target instanceof DmoSnapshot) {
            //持久层往领域层转换
            ((DmoSnapshot) target).setSnapshot(source);
        }
    }

    default void cleanPO(PoSnapshot target) {
        Object snapshot = target.getSnapshot();
        if (null == snapshot) {
            target.setChanged(true);
            return;
        }
        Field[] fields = target.getClass().getDeclaredFields();
        for (Field field : fields) {
            String fieldName = field.getName();
            if (IGNORE_FIELDS.contains(fieldName)) {
                continue;
            }
            field.setAccessible(true);
            try {
                Object newValue = field.get(target);
                Object oldValue = field.get(snapshot);
                if (Objects.equals(newValue, oldValue)) {
                    field.set(target, null);
                } else {
                    target.setChanged(true);
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}

4.DO-PO转换器继承SnapshotConverter

转换完毕后,会自动执行after方法

java 复制代码
@Mapper
public interface CustomerDmo2PoConverter extends SnapshotConverter {
    CustomerDmo2PoConverter INSTANCE = Mappers.getMapper(CustomerDmo2PoConverter.class);

    @Mapping(target = "userName", source = "source.userName.userName")
    @Mapping(target = "password", source = "source.password.password")
    @Mapping(target = "phone", source = "source.phone.phone")
    @Mapping(target = "email", source = "source.email.email")
    CustomerPO toCustomerPO(Customer source);

    @InheritInverseConfiguration(name = "toCustomerPO")
    Customer toDmo(CustomerPO po);
}

5.转换方法使用

在save或其他更新方法时,转换完成后,如果changed标记为false,直接返回成功,不继续持久化

ini 复制代码
@Override
public Customer save(Customer customer) {
    CustomerPO po = CustomerDmo2PoConverter.INSTANCE.toCustomerPO(customer);
    if (!po.getChanged()) {
        return customer;
    }
    boolean success;
    if (null == po.getId()) {
        success = save(po);
        if (success) {
            customer.setId(po.getId());
        }
    } else {
        success = updateById(po);
    }
    if (!success) {
        throw new BException(RCodeEnum.PERSIST_OBJECT_ERROR);
    }
    return customer;
}

6.示例

以更新用户昵称为例

6.1 app层实现

应用层接收到入参为用户id和用户昵称,调用repo的findById方法得到领域对象后,设置用户昵称,然后调用save方法持久化即可

java 复制代码
@Service
public class CustomerAppService {
    ...

    @Transactional(rollbackFor = Exception.class)
    public Long updateNickName(UpdateNickNameCmd cmd) {
        Customer customer = customerRepo.findById(cmd.getId());
        customer.setNickName(cmd.getNickName());
        customerRepo.save(customer);
        return customer.getId();
    }
}

6.2 repo实现

  1. repo层findById拿到PO对象后,调用CustomerDmo2PoConvertertoDmo方法,得到领域对象的同时,也把PO作为镜像存储到DO中
  2. repo层的save方法,调用CustomerDmo2PoConvertertoCustomerPO方法,将DO的镜像传给PO,同时清空PO的未变更属性
java 复制代码
@Override
public Customer save(Customer customer) {
    CustomerPO po = CustomerDmo2PoConverter.INSTANCE.toCustomerPO(customer);
    if (!po.getChanged()) {
        return customer;
    }
    ...
    return customer;
}

@Override
public Customer findById(Serializable id) {
    CustomerPO po = getById(id);
    return CustomerDmo2PoConverter.INSTANCE.toDmo(po);
}

6.3 测试

可以看到,update语句,只更新nick_name这一个字段,再次在postman点击发送,值不变的情况下,没有执行update语句。

yaml 复制代码
selectById    : ==>  Preparing: SELECT id,user_name,nick_name,password,phone,email FROM cuc_customer WHERE id=?
selectById    : ==> Parameters: 1743128870491926530(String)
selectById    : <==      Total: 1
updateById    : ==>  Preparing: UPDATE cuc_customer SET nick_name=? WHERE id=?
updateById    : ==> Parameters: 小土豆(String), 1743128870491926530(Long)
updateById    : <==    Updates: 1

7.小结

  1. 本方案通过mapstruct和反射,保证了只更新必要字段;
  2. 但是因为清空了PO的非变更字段,持久化后,PO对象不可重新转换为DO对象,否则会造成属性不全,应通过重查DB得到新的镜像来执行相关操作;
  3. 也可以通过mybatis的切面在没生成sql的时候进行清除未变更字段的操作,这样对PO的影响更小。
  4. 项目源码
相关推荐
ai小鬼头8 小时前
AIStarter教你快速打包GPT-SoVITS-v2,解锁AI应用市场新玩法
前端·后端·github
paopaokaka_luck9 小时前
基于SpringBoot+Vue的汽车租赁系统(协同过滤算法、腾讯地图API、支付宝沙盒支付、WebsSocket实时聊天、ECharts图形化分析)
vue.js·spring boot·后端·websocket·算法·汽车·echarts
giao源9 小时前
Spring Boot 整合 Shiro 实现单用户与多用户认证授权指南
java·spring boot·后端·安全性测试
【本人】9 小时前
Django基础(四)———模板常用过滤器
后端·python·django
豌豆花下猫10 小时前
Python 潮流周刊#111:Django迎来 20 周年、OpenAI 前员工分享工作体验(摘要)
后端·python·ai
LaoZhangAI11 小时前
ComfyUI集成GPT-Image-1完全指南:8步实现AI图像创作革命【2025最新】
前端·后端
LaoZhangAI11 小时前
Cline + Gemini API 完整配置与使用指南【2025最新】
前端·后端
LaoZhangAI11 小时前
Cline + Claude API 完全指南:2025年智能编程最佳实践
前端·后端