Java实现DDD中UnitOfWork

背景

Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.
Unit of Work --Martin Fowler

Unit Of Work模式,由马丁大叔提出,是一种数据访问模式。UOW模式的作用是在业务用例的操作中跟踪对象的所有更改(增加、删除和更新),并将所有更改的对象保存在其维护的列表中。在业务用例的终点,通过事务,一次性提交所有更改,以确保数据的完整性和有效性。总而言之,UOW协调这些对象的持久化及并发问题。

Uow的本质

  1. UOW跟踪变化
  2. UOW维护了一个变更列表
  3. UOW将跟踪到的已变更的对象保存到变更列表中
  4. UOW借助事务一次性提交变更列表中的所有更改
  5. UOW处理并发

对于以上这些点,在C#的EF框架中,DBContext已经实现。
而这里主要描述如何用java实现以上要点。

Repository

  • 将仓储Repo作为聚合的范型类
  • 在Repo中维护一个聚合与聚合状态的集合
  • 在Repo中每次add/update/delete等操作时,将操作的聚合对象,和其最终状态存入集合中
  • 在Repo中的retrieve方法,将聚合检索出来并存入Repo的集合中
  • 代码如下:
public class RepositoryBase implements IRepository {

    private HashMap> map = new HashMap>();

    private Class tClass;

    public RepositoryBase(Class tClass) {
        RepositoryThreadLocalHelper.newInstance().put(tClass.getSimpleName(), this);
        this.tClass = tClass;
    }

    public void add(T t) {
        IBusinessKey bk = t.getBusinessKey();
        if (map.containsKey(bk) && RepositoryComponentState.DELETED.equals(map.get(bk).getState())) {
            map.put(bk, new RepositoryComponent(t, RepositoryComponentState.UNCHANGED));
        } else {
            map.put(bk, new RepositoryComponent(t, RepositoryComponentState.ADDED));
        }
    }

    public void update(T t) {
        IBusinessKey bk = t.getBusinessKey();
        if (map.containsKey(bk) && RepositoryComponentState.ADDED.equals(map.get(bk).getState())) {
            map.put(bk, new RepositoryComponent(t, RepositoryComponentState.ADDED));
        } else {
            map.put(bk, new RepositoryComponent(t, RepositoryComponentState.MODIFIED));
        }
    }

    public void delete(IBusinessKey bk) {
        if (map.containsKey(bk) && RepositoryComponentState.ADDED.equals(map.get(bk).getState())) {
            map.get(bk).setState(RepositoryComponentState.UNCHANGED);
        } else {
            map.put(bk, new RepositoryComponent(retrieve(bk), RepositoryComponentState.DELETED));
        }
    }

    public void delete(T t) {
        IBusinessKey bk = t.getBusinessKey();
        if (map.containsKey(bk) && RepositoryComponentState.ADDED.equals(map.get(bk).getState())) {
            map.get(bk).setState(RepositoryComponentState.UNCHANGED);
        } else {
            map.put(bk, new RepositoryComponent(t, RepositoryComponentState.DELETED));
        }
    }

    public T retrieve(IBusinessKey bk) {
        if (map.containsKey(bk)) {
            return map.get(bk).getT();
        } else {
            RepositoryBuilder builder = new RepositoryBuilder();
            T t = builder.buildBo(tClass, bk);
            map.put(bk, new RepositoryComponent(t, RepositoryComponentState.UNCHANGED));
            return t;
        }
    }
}

RepositoryComponentState

  • 聚合存在于内存中的状态,主要分为以下5个状态
Added	4	
The entity is being tracked by the context but does not yet exist in the database.
Deleted	2	
The entity is being tracked by the context and exists in the database. It has been marked for deletion from the database.
Detached	0	
The entity is not being tracked by the context.
Modified	3	
The entity is being tracked by the context and exists in the database. Some or all of its property values have been modified.
Unchanged	1	
The entity is being tracked by the context and exists in the database. Its property values have not changed from the values in the database.

Uow

  • 增加自定义注解UowTransaction
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UowTransaction {
}
  • 增加注解切面,在所注解的方法上执行完毕后调用uowService.saveChange()
@Slf4j
@Component
public class UowService {

    @Transaction
    public void saveChange() {
        SqlSessionFactory factory = (SqlSessionFactory) SpringContextUtil.getBean("sqlSessionFactory");
        SqlSession session = factory.openSession();

        Collection repos = RepositoryThreadLocalHelper.newInstance().getRepositories();
        for (IRepository repo : repos) {
            repo.flushDb(session);
        }
    }
}

RepositoryFlush

  • flushDb时将内存中的聚合集合统一刷新入数据库
  • flushAdd:将聚合从聚合根到所有子节点依次入库
  • flushModify:更改数据库聚合时,先检索出数据库中原聚合originEntity,然后将原聚合与更改后聚合依次对比,子节点中根据对比内容做新增/删除/修改
  • flushDelete:将聚合从聚合根到所有子节点依次删除
  • 具体参考代码如下:
@Slf4j
public class RepositoryFlush {

    private SqlSession session;
    private RepositoryConfigHelper configHelper = new RepositoryConfigHelper();

    RepositoryFlush(SqlSession session) {
        this.session = session;
    }

    void flushAdd(IBusinessEntity bo) {
        RepositoryConfig repoConfig = configHelper.getRepositoryConfig(bo);
        //bo->do
        Object dataObject = TypeConversion.unWrapPrimitiveType(bo, repoConfig.getEntityClass());
        BaseMapper mapper = this.session.getMapper(repoConfig.getEntityMapperClass());

        mapper.insert(dataObject);

        List childNames = BusinessObjectManager.getEntityNamesByComposition(
                bo.getBusinessObjectRoot().getArtifactName(),
                bo.getArtifactName());
        if (childNames.size() > 0) {
            for (String name : childNames) {
                List childBes = bo.getEntitiesByComposition(name);
                for (IBusinessEntity be : childBes) {
                    flushAdd(be);
                }
            }
        }
    }

    void flushModify(IBusinessEntity originBo, IBusinessEntity bo) {
        RepositoryConfig repoConfig = configHelper.getRepositoryConfig(bo);
        // bo->do
        Object dataObject = TypeConversion.unWrapPrimitiveType(bo, repoConfig.getEntityClass());
        BaseMapper mapper = this.session.getMapper(repoConfig.getEntityMapperClass());

        //1. gdt->基本类型
        //2. bk:驼峰形式改为下划线形式
        Map bkMap = RepositoryUtil.MapCamelCaseToUnderscore(
                UnWrapMapUtil.getGdtValue(
                        bo.getBusinessKey().getBusinessKeyMap()));

        // update root
        UpdateWrapper updateWrapper = new UpdateWrapper();
        updateWrapper.allEq(bkMap);
        mapper.update(dataObject, updateWrapper);

        // 遍历子be
        List originChildNames = BusinessObjectManager.getEntityNamesByComposition(
                originBo.getBusinessObjectRoot().getArtifactName(),
                originBo.getArtifactName()
        );
        if (originChildNames.size() > 0) {
            for (String name : originChildNames) {
                List originChildBes = originBo.getEntitiesByComposition(name);
                List childBes = bo.getEntitiesByComposition(name);
                for (IBusinessEntity be : childBes) {
                    // be在数据库中,从originChildBes列表中删除,所有删除完剩下的->需要delete的
                    Optional optional = originChildBes.stream()
                            .filter(
                                    x -> {
                                        // 判断bk是否相同
                                        return x.getBusinessKey().equals(be.getBusinessKey());
                                    }
                            ).findFirst();
                    if (optional.isPresent()) {
                        // 数据库中存在:修改
                        IBusinessEntity originBe = optional.get();
                        originChildBes.remove(originBe);
                        flushModify(originBe, be);
                    } else {
                        // 数据库中不存在:新增
                        flushAdd(be);
                    }
                }
                // 数据库中存在,但modifyBo中没有:删除
                if (originChildBes.size() > 0) {
                    for (IBusinessEntity originDeleteBe : originChildBes) {
                        flushDelete(originDeleteBe);
                    }
                }
            }
        }
    }

    void flushDelete(IBusinessEntity bo) {
        RepositoryConfig repoConfig = configHelper.getRepositoryConfig(bo);
        BaseMapper mapper = this.session.getMapper(repoConfig.getEntityMapperClass());

        IBusinessKey bk = bo.getBusinessKey();
        Map bkMap = RepositoryUtil.MapCamelCaseToUnderscore(
                UnWrapMapUtil.getGdtValue(
                        bo.getBusinessKey().getBusinessKeyMap()));

        mapper.deleteByMap(bkMap);

        List childNames = BusinessObjectManager.getEntityNamesByComposition(
                bo.getBusinessObjectRoot().getArtifactName(),
                bo.getArtifactName()
        );
        if (childNames.size() > 0) {
            for (String name : childNames) {
                List childBes = bo.getEntitiesByComposition(name);
                for (IBusinessEntity be : childBes) {
                    flushDelete(be);
                }
            }
        }
    }
}
  • 在repo中增加flushDb方法,如下:
public void flushDb(SqlSession session) {
    RepositoryFlush flush = new RepositoryFlush(session);
    for (Map.Entry> entry : map.entrySet()) {
        RepositoryComponentState state = entry.getValue().getState();
        T t = entry.getValue().getT();
        if (RepositoryComponentState.ADDED.equals(state)) {
            flush.flushAdd(t);
        } else if (RepositoryComponentState.MODIFIED.equals(state)) {
            RepositoryBuilder builder = new RepositoryBuilder();
            T rootT = builder.buildBo(tClass, t.getBusinessKey());
            flush.flushModify(rootT, t);
        } else if (RepositoryComponentState.DELETED.equals(state)) {
            flush.flushDelete(t);
        }
    }
}

Retrieve&&RepositoryBuilder

  • repo中提供retrieve方法用于检索聚合
  • 若聚合在repo的集合中已存在则直接返回聚合,若无聚合,则通过RepoBuilder从数据库中捞取聚合
public class RepositoryBuilder {

    private RepositoryConfigHelper configHelper = new RepositoryConfigHelper();
    private Map> beValues = new HashMap<>();

    T buildBo(Class tClass, IBusinessKey businessKey) {
        SqlSessionFactory factory = (SqlSessionFactory) SpringContextUtil.getBean("sqlSessionFactory");
        SqlSession session = factory.openSession();
        try {
            RepositoryConfig config = configHelper.getRepositoryConfig((IBusinessEntity) tClass.newInstance());
            BaseMapper mapper = session.getMapper(config.getEntityMapperClass());
            QueryWrapper queryWrapper = new QueryWrapper();
            queryWrapper.allEq(
                    RepositoryUtil.MapCamelCaseToUnderscore(
                            UnWrapMapUtil.getGdtValue(
                                    businessKey.getBusinessKeyMap()))
            );
            // 获取到当前do object
            Object object = mapper.selectOne(queryWrapper);
            if (object == null) {
                throw new RuntimeException("未找到数据库对应DO数据。");
            }
            // build child
            IBusinessObjectRoot rootT = (IBusinessObjectRoot) TypeConversion.wrapPrimitiveType(object, tClass);
            buildChildBe(session, tClass, object, rootT);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        } finally {
            session.close();
        }
        // beValues->bo
        return (T) BusinessObjectManager.newInstance().createBusinessObjectInstance(
                tClass.getSimpleName(),
                beValues
        );
    }

    /**
     * 1. do->beValue
     * 2. beValues.add(beValue)
     * 3. bo查子bo
     * 4. 返回
     */
    private void buildChildBe(SqlSession session, Class tClass, Object object, IBusinessObjectRoot rootT)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        T t = TypeConversion.wrapPrimitiveType(object, tClass);
        if (t == null) {
            throw new RuntimeException("DO数据类型转BE异常");
        }
        // 加入beValues
        if (beValues.containsKey(t.getArtifactName())) {
            beValues.get(t.getArtifactName()).add(t);
        } else {
            beValues.put(t.getArtifactName(), new ArrayList() {{
                add(t);
            }});
        }
        //
        IBusinessKey bk = t.getBusinessKey();
        List childClassNames = BusinessObjectManager.getEntityNamesByComposition(
                rootT.getArtifactName(),
                t.getArtifactName()
        );
        if (childClassNames.size() > 0) {
            for (String childClassName : childClassNames) {
                Class childClass = Class.forName(childClassName);
                // 构造函数:包含父be(无结构构造函数)
                IBusinessEntity nullChildBe = (IBusinessEntity) childClass
                        .getConstructor()
                        .newInstance();
                RepositoryConfig childConfig = configHelper.getRepositoryConfig(rootT, nullChildBe);
                BaseMapper childMapper = session.getMapper(childConfig.getEntityMapperClass());
                List dbList = childMapper.selectByMap(
                        RepositoryUtil.MapCamelCaseToUnderscore(
                                UnwrapMapUtil.getGdtValue(bk.getBusinessKeyMap())
                        )
                );
                for (Object dbObject : dbList) {
                    buildChildBe(session, childClass, dbObject, rootT);
                }
            }
        }
    }
}

最后

  • 以上代码只包含Uow、Repo等关键代码,完整代码使用还需要配合聚合的建模,全局统一类型的使用
  • 代码仅供学习,以后有机会会上传到github中

资料参考
EntityFrameworkCore
UnitOfWork知多少

最后修改:2020 年 05 月 21 日 04 : 08 PM
如果觉得我的文章对你有用,请随意赞赏