跳转到内容

领域标识(Domain Identity)

🧑‍💻 作者: yang
📦 版本: 0.0.1
📄 字数(字): 0
⏳ 时长(min): 0
📅 发表于: 2026-01-26
⏱️ 更新于: 2026-01-28

什么是领域标识

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;  

}

EntityUtil.java

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();
}

ObjectCode.java

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);
    }

}

SignGeneratorHandler.java

SignGeneratorHandler #generateSign
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);
}

领域标识与主键生成

由于领域标识具有稳定性和唯一性, 那么结合数据库表名可唯一确定数据库表中的主键。

EntityUtil.java

EntityUtil #getTableName
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"));
}

SignGeneratorHandler.java

SignGeneratorHandler #generatePrimaryKey
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);
    }

}

由一可爱小白兔支持