领域标识(Domain Identity)
什么是领域标识
What
领域标识是用于在领域模型中唯一标识一个实体(Entity)的属性或属性组合。
Note
- 标识由业务规则决定,而非技术实现;
- 标识应具有稳定性和唯一性;
- 通常只用于聚合根(
Aggregate Root),因为聚合内部的实体可通过局部路径识别。
如何设计领域标识
自然标识(Natural Identity) :由业务天然提供的唯一标识。
如:
- 用户:身份证号(在某些国家/场景下)
- 商品:SKU 编码
- 书本:ISBN
合成标识(Composite Identity):多个字段组合构成唯一性。
如:
- 仓库ID + 货架ID + 层号 唯一标识一个货位
代理标识(Surrogate Identity) :由系统生成。
如:
- roleCode:role_20260110_1
领域标识的生成
无论是什么标识,都可以以下策略生成:在聚合根的实体字段上添加@Code
生成规则如下:
- 如果所有字段未指定 order 那么默认为 0, 多个为 0 的根据字段名(unicode)排序
- 如果所有字段都指定 order, 建议从 1 开始递增 ,因为预留给 mainRefKey 的 order 为 0, 则按照 order 递增的顺序排序
- 混合, 既有指定的 order,又有未指定的 order, 则先按照 order 递增排序, 多个 order 值相同的根据字段名(unicode)排序
拓展
ObjectCode: 用于标识对象的唯一接口,所有的xxObjectCode都要继承该接口
分为如下类型:
- SimpleObjectCode:适用于单一的标识,如
roleCode,idCard CustomObjectCode:适用于多个标识, 如UserObjectCode中的userName+gender+ ...
参考代码
Code.java
java
@Documented
@Retention(RUNTIME)
@Target({FIELD})
public @interface Code {
int order() default 0;
}java
private static final Map<String, List<Field>> FIELD_CACHE = new ConcurrentHashMap<>();
/**
* 获取字段列表
*
* @param clazz 实体类
* @param annotation 注解
* @return 字段列表
*/
public static List<Field> getFields(Class<?> clazz, Class<? extends Annotation> annotation) {
String key = clazz.getName() + "@" + (annotation == null ? "all" : annotation.getName());
return FIELD_CACHE.computeIfAbsent(key, value -> {
List<Field> result = new ArrayList<>();
Class<?> current = clazz;
while (current != null && current != Object.class) {
for (Field field : current.getDeclaredFields()) {
if (annotation == null || field.isAnnotationPresent(annotation)) {
result.add(field);
}
}
current = current.getSuperclass();
}
return result;
});
}java
/**
* 获取有{@link Code}字段属性值列表:先按 order 排序,再按照字段名排序, 保证稳定性
*
* @param entity 实体对象
* @param fieldList 字段列表
* @return 字段属性值列表
*/
public static List<Object> getFieldWithCodeValues(@NonNull Object entity, List<Field> fieldList) {
return fieldList.stream()
.sorted(Comparator.comparingInt((Field f) -> f.getAnnotation(Code.class).order())
.thenComparing(Field::getName)
)
.map(field -> {
try {
return field.get(entity);
} catch (IllegalAccessException e) {
throw new UtilException("Cannot access field: " + field.getName(), e);
}
}).toList();
}java
public interface ObjectCode {
/**
* 获取字段属性值列表
*
* @return 字段属性值列表
*/
default String getCodeValues() {
List<Field> codeFields = EntityUtil.getFields(this.getClass(), Code.class);
List<?> fieldWithCodeValues = EntityUtil.getFieldWithCodeValues(this, codeFields);
return StrUtil.join(fieldWithCodeValues, StrPool.DOT);
}
}java
/**
* 生成字段属性值哈希
*
* @param fieldValueList 字段属性值列表
* @return md5Hash
*/
public String generateSign(@NonNull List<?> fieldValueList) {
if (CollectionUtil.isEmpty(fieldValueList)) {
return StrPool.EMPTY;
}
String joinStr = StrUtil.join(fieldValueList, StrPool.DOT);
return DigestUtil.md5(joinStr);
}领域标识与主键生成
由于领域标识具有稳定性和唯一性, 那么结合数据库表名可唯一确定数据库表中的主键。
java
/**
* 获取表名
*
* @param clazz 实体类
* @return 表名
*/
public static String getTableName(Class<? extends BaseDO> clazz) {
return Optional.ofNullable(clazz.getAnnotation(TableName.class))
.map(TableName::value)
.filter(StrUtil::isNotEmpty)
.orElseThrow(() -> new NoSuchElementException("failed to get table name"));
}java
/**
* 生成主键
*
* @param tableName 表名
* @param fieldValueList 字段属性值列表
* @return 表名.md5Hash
*/
public String generatePrimaryKey(@NonNull String tableName, @NonNull List<?> fieldValueList) {
return tableName + StrPool.DOT + this.generateSign(fieldValueList);
}结合Mybatis-Plus 字段填充, 实现主键生成
MyMetaObjectHandler.java
java
public class MyMetaObjectHandler implements MetaObjectHandler {
@Resource
private SignGeneratorHandler signGeneratorHandler;
/**
* 新增时填充字段
*
* @param metaObject {@link MetaObject}
*/
@Override
public void insertFill(MetaObject metaObject) {
setFieldValByName(RRN, this.setPrimaryKeyField(metaObject), metaObject);
// 省略其他字段
}
/**
* 插入时主键字段
*
* @param metaObject {@link MetaObject}
* @return 主键 id
*/
private String setPrimaryKeyField(MetaObject metaObject) {
Object entity = metaObject.getOriginalObject();
if (Objects.isNull(entity)) {
throw new IllegalArgumentException("Entity is null");
}
// 判断 entity 是否继承 BaseDO
if (!(entity instanceof BaseDO)) {
throw new IllegalArgumentException("Entity must extend BaseDO, but got: " + entity.getClass());
}
Class<? extends BaseDO> clazz = entity.getClass().asSubclass(BaseDO.class);
// 获取所有 @Code 注解的字段
List<Field> codeFields = EntityUtil.getFields(clazz, Code.class);
if (CollectionUtil.isEmpty(codeFields)) {
throw new IllegalStateException("No @Code field found in " + clazz.getName());
}
// 获取字段值
List<?> fieldWithCodeValues = getFieldWithCodeValues(entity, codeFields);
return signGeneratorHandler.generatePrimaryKey(EntityUtil.getTableName(clazz), fieldWithCodeValues);
}
}