(二)边学边用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. 项目源码
相关推荐
a努力。1 小时前
【基础数据篇】数据等价裁判:Comparer模式
java·后端
开心猴爷1 小时前
苹果App Store应用程序上架方式全面指南
后端
小飞Coding1 小时前
三种方式打 Java 可执行 JAR 包,你用对了吗?
后端
bcbnb1 小时前
没有 Mac,如何在 Windows 上架 iOS 应用?一套可落地的工程方案
后端
用户8356290780511 小时前
从一维到二维:用Spire.XLS轻松将Python列表导出到Excel
后端·python
哈哈哈笑什么1 小时前
SpringBoot 企业级接口加密【通用、可配置、解耦的组件】「开闭原则+模板方法+拦截器/中间件模式」
java·后端·安全
期待のcode1 小时前
springboot依赖管理机制
java·spring boot·后端
我是小妖怪,潇洒又自在2 小时前
springcloud alibaba(八)链路追踪
后端·spring·spring cloud·sleuth·zipkin
疯狂的程序猴2 小时前
深入理解 iPhone 文件管理,从沙盒结构到开发调试的多工具协同实践
后端