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的本質
- UOW跟蹤變化
- UOW維護了一個變更列表
- UOW將跟蹤到的已變更的物件儲存到變更列表中
- UOW藉助事務一次性提交變更列表中的所有更改
- UOW處理併發
對於以上這些點,在C#的EF框架中,DBContext已經實現。
而這裡主要描述如何用java實現以上要點。
Repository
- 將倉儲Repo作為聚合的範型類
- 在Repo中維護一個聚合與聚合狀態的集合
- 在Repo中每次add/update/delete等操作時,將操作的聚合物件,和其最終狀態存入集合中
- 在Repo中的retrieve方法,將聚合檢索出來並存入Repo的集合中
- 程式碼如下:
public class RepositoryBase<T extends IBusinessObjectRoot> implements IRepository<T> {
private HashMap<IBusinessKey, RepositoryComponent<T>> map = new HashMap<IBusinessKey, RepositoryComponent<T>>();
private Class<T> tClass;
public RepositoryBase(Class<T> 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>(t, RepositoryComponentState.UNCHANGED));
} else {
map.put(bk, new RepositoryComponent<T>(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>(t, RepositoryComponentState.ADDED));
} else {
map.put(bk, new RepositoryComponent<T>(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<T>(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>(t, RepositoryComponentState.DELETED));
}
}
public T retrieve(IBusinessKey bk) {
if (map.containsKey(bk)) {
return map.get(bk).getT();
} else {
RepositoryBuilder<T> builder = new RepositoryBuilder<T>();
T t = builder.buildBo(tClass, bk);
map.put(bk, new RepositoryComponent<T>(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<IRepository> 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<String> childNames = BusinessObjectManager.getEntityNamesByComposition(
bo.getBusinessObjectRoot().getArtifactName(),
bo.getArtifactName());
if (childNames.size() > 0) {
for (String name : childNames) {
List<IBusinessEntity> 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<String, String> bkMap = RepositoryUtil.MapCamelCaseToUnderscore(
UnWrapMapUtil.getGdtValue(
bo.getBusinessKey().getBusinessKeyMap()));
// update root
UpdateWrapper updateWrapper = new UpdateWrapper();
updateWrapper.allEq(bkMap);
mapper.update(dataObject, updateWrapper);
// 遍歷子be
List<String> originChildNames = BusinessObjectManager.getEntityNamesByComposition(
originBo.getBusinessObjectRoot().getArtifactName(),
originBo.getArtifactName()
);
if (originChildNames.size() > 0) {
for (String name : originChildNames) {
List<IBusinessEntity> originChildBes = originBo.getEntitiesByComposition(name);
List<IBusinessEntity> childBes = bo.getEntitiesByComposition(name);
for (IBusinessEntity be : childBes) {
// be在資料庫中,從originChildBes列表中刪除,所有刪除完剩下的->需要delete的
Optional<IBusinessEntity> 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<String, String> bkMap = RepositoryUtil.MapCamelCaseToUnderscore(
UnWrapMapUtil.getGdtValue(
bo.getBusinessKey().getBusinessKeyMap()));
mapper.deleteByMap(bkMap);
List<String> childNames = BusinessObjectManager.getEntityNamesByComposition(
bo.getBusinessObjectRoot().getArtifactName(),
bo.getArtifactName()
);
if (childNames.size() > 0) {
for (String name : childNames) {
List<IBusinessEntity> 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<IBusinessKey, RepositoryComponent<T>> 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<T> builder = new RepositoryBuilder<T>();
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<T extends IBusinessObjectRoot> {
private RepositoryConfigHelper configHelper = new RepositoryConfigHelper();
private Map<String, List<IBusinessEntity>> beValues = new HashMap<>();
T buildBo(Class<T> 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<T> 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<IBusinessEntity>() {{
add(t);
}});
}
//
IBusinessKey bk = t.getBusinessKey();
List<String> 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中