Skip to content

约定

约定是团队的默契,为我们的工作提供一致性和可预测性。统一的规则和准则能减少沟通成本,让代码、数据库和文档更容易长期维护。

v2.0.0 起,后端同时支持 MySQL 和 PostgreSQL。本页中的 SQL 示例不再以 MySQL 单一方言为默认前提;涉及表结构和初始化数据时,应优先使用 Liquibase XML 结构化写法。

后端

1. 逻辑删除

逻辑删除字段仍统一使用 T / F 表示:

含义
F正常数据
T已逻辑删除

v2.0.0 同时支持 MySQL 和 PostgreSQL,因此新表结构不再推荐使用 MySQL 专属的 enum('T','F') 字段类型。推荐使用跨库友好的字符字段。

Liquibase changelog 推荐写法:

xml
<column name="del_flag" type="${bool.type}" defaultValue="F" remarks="删除与否">
    <constraints nullable="false"/>
</column>
<column name="delete_id" type="${bigint.type}" remarks="删除人"/>
<column name="delete_time" type="${datetime.type}" remarks="删除时间"/>

临时手写 SQL 时,可按数据库分别处理:

sql
del_flag char(1) NOT NULL DEFAULT 'F' COMMENT '删除与否',
delete_id bigint DEFAULT NULL COMMENT '删除人',
delete_time datetime DEFAULT NULL COMMENT '删除时间'
sql
del_flag char(1) NOT NULL DEFAULT 'F',
delete_id bigint,
delete_time timestamp

WARNING

不建议再把 Java enum 或数据库 enum 直接作为业务状态落库。跨库字段请优先使用 char / varchar / bigint 等通用类型;业务含义通过 数据字典sz-module-common 中的字典常量表达,避免后续 MySQL / PostgreSQL 迁移困难。

PO 类中需要通过 @Column(isLogicDelete = true) 指定逻辑删除字段:

java
@Table("sys_user")
@Schema(description = "系统用户表")
public class SysUser implements Serializable {

    @Column(isLogicDelete = true) // 通过 @Column 指定逻辑删除属性
    private String delFlag;

    // ...
}

MyBatis-Flex 的逻辑删除值由各环境 mybatis-flex.yml 统一配置:

yaml
mybatis-flex:
  global-config:
    deleted-value-of-logic-delete: "T" # 逻辑删除字段的已删除值
    normal-value-of-logic-delete: "F"  # 逻辑删除字段的未删除值

逻辑删除填充

框架提供逻辑删除数据填充能力。v2.0.0 的实现以实体上的 @LogicDeleteFill 为开关;只有实体标注该注解,并且表中存在注解指定的删除时间/删除人字段时,才会自动追加填充值。

java
@LogicDeleteFill(deleteTimeColumn = "delete_time", deleteByColumn = "delete_id")
@Table("sys_user")
public class SysUser implements Serializable {
    // ...
}

核心处理逻辑位于 EntityLogicDeleteListener:框架会读取实体上的 @LogicDeleteFill,使用当前数据库方言包装列名,并在逻辑删除 SQL 中追加删除时间和删除人。当前实现已将删除时间表达式统一为 CURRENT_TIMESTAMP,避免继续依赖 MySQL 风格的 now(),更适合 MySQL / PostgreSQL 以及多数据源场景。

java
public class EntityLogicDeleteListener extends DefaultLogicDeleteProcessor {

    @Override
    public String buildLogicDeletedSet(String logicColumn, TableInfo tableInfo, IDialect iDialect) {
        StringBuilder sqlBuilder = new StringBuilder();
        Class<?> entityClass = tableInfo.getEntityClass();
        LogicDeleteFill annotation = entityClass.getAnnotation(LogicDeleteFill.class);

        sqlBuilder.append(iDialect.wrap(logicColumn))
                .append(EQUALS)
                .append(prepareValue(getLogicDeletedValue()));

        if (annotation == null) {
            return sqlBuilder.toString();
        }

        String deleteTimeCol = annotation.deleteTimeColumn();
        String deleteByCol = annotation.deleteByColumn();
        List<String> columns = Arrays.asList(tableInfo.getAllColumns());

        if (!deleteTimeCol.isEmpty() && columns.contains(deleteTimeCol)) {
            sqlBuilder.append(", ")
                    .append(iDialect.wrap(deleteTimeCol))
                    .append(EQUALS)
                    .append(" CURRENT_TIMESTAMP");
        }

        if (!deleteByCol.isEmpty() && isLogin() && columns.contains(deleteByCol)) {
            Object loginId = StpUtil.getStpLogic().getLoginId();
            sqlBuilder.append(", ")
                    .append(iDialect.wrap(deleteByCol))
                    .append(EQUALS)
                    .append(prepareValue(loginId));
        }

        return sqlBuilder.toString();
    }

    private static Object prepareValue(Object value) {
        if (value == null) {
            return "NULL";
        }
        return (!(value instanceof Number) && !(value instanceof Boolean))
                ? "'" + value.toString().replace("'", "''") + "'"
                : value;
    }
}

NOTE

这里不要改回 now()、反引号列名或拼接未转义的字符串值。iDialect.wrap(...) 负责适配不同数据库的列名包装方式,CURRENT_TIMESTAMP 是更通用的当前时间表达式,prepareValue(...)null 和字符串单引号做了处理,能降低多数据源和跨数据库运行时的兼容风险。

2. 数据填充

当表需要记录创建人、创建时间、更新人、更新时间时,推荐使用以下字段命名。系统会根据属性命名匹配并填充:

数据库字段Java 属性Java 类型
create_idcreateIdLong
create_timecreateTimeLocalDateTime
update_idupdateIdLong
update_timeupdateTimeLocalDateTime

Liquibase changelog 推荐写法:

xml
<column name="create_time" type="${datetime.type}" defaultValueComputed="${now.function}" remarks="创建时间"/>
<column name="update_time" type="${datetime.type}" defaultValueComputed="${now.function}" remarks="更新时间"/>
<column name="create_id" type="${bigint.type}" remarks="创建人"/>
<column name="update_id" type="${bigint.type}" remarks="更新人"/>

临时手写 SQL 示例:

sql
create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
create_id bigint DEFAULT NULL COMMENT '创建人',
update_id bigint DEFAULT NULL COMMENT '更新人'
sql
create_time timestamp DEFAULT CURRENT_TIMESTAMP,
update_time timestamp DEFAULT CURRENT_TIMESTAMP,
create_id bigint,
update_id bigint

PO 类需要在 @Table 中启用监听器:

java
@Table(value = "sys_client", onInsert = EntityChangeListener.class, onUpdate = EntityChangeListener.class)
public class SysClient implements Serializable {

    @Schema(description = "创建人")
    private Long createId;

    @Schema(description = "创建时间")
    private LocalDateTime createTime;

    @Schema(description = "更新人")
    private Long updateId;

    @Schema(description = "更新时间")
    private LocalDateTime updateTime;
}

EntityChangeListener 会在新增时填充 createIdcreateTime,在更新时填充 updateIdupdateTime。如果实体中不存在对应属性,监听器会忽略该字段并记录日志。

3. SQL 的编写

框架集成了 MyBatis-Flex,并在 v2.0.0 补齐了 MySQL / PostgreSQL 双数据库支持。因此 SQL 编写不再只考虑 MySQL。

推荐优先级如下:

  1. 普通查询优先使用 MyBatis-Flex QueryWrapper / QueryChain,减少手写方言 SQL。
  2. 复杂动态查询或性能敏感查询可以放到 Mapper XML。若模块需要同时兼容 MySQL 和 PostgreSQL,应避免 GROUP_CONCATFIND_IN_SETINSERT IGNORE、反引号、MySQL enum 等单库写法;若项目已固定目标数据库,可按目标库能力编写,但建议在文件或方法注释中说明数据库前提。
  3. 初始化数据、菜单、字典、角色权限等脚本优先使用 Liquibase XML 的结构化标签,例如 <createTable><insert><column>,不要优先写裸 SQL 或 CDATA。
  4. 如果确实存在数据库方言差异,固定单库项目可按目标库维护;需要双库兼容时,应在 Liquibase 中使用 dbms="mysql" / dbms="postgresql" 分别维护,或在代码层通过数据库模块中的方言类隔离。

IMPORTANT

v2.0.0 已废弃“把枚举类型直接落到数据库字段”的写法。数据库保存稳定的编码值,例如 T/F、字典项 ID、字典项 code 或业务常量值;Java 侧通过字典常量、响应枚举、@DictFormat 或业务转换层表达语义。需要用户可维护、前端可展示的状态值,应优先进入 sys_dict_source / sys_dict_type / sys_dict 字典体系。

常见替换建议:

旧写法v2.0.0 推荐写法原因
enum('T','F')char(1) + mybatis-flex.global-config 约定 T/F兼容 MySQL / PostgreSQL
Java enum 名称直接入库字典项 ID / code 或 sz-module-common 常量值避免枚举重命名导致历史数据失效
INSERT INTO 初始化菜单/字典Liquibase XML <insert>跨库、可追踪、可回放
MySQL 专属聚合函数固定 MySQL 时可使用;双库兼容时改为 Java 层组装或按方言拆分避免把单库能力误写成通用能力

数据字典约定见 数据字典,数据库切换注意事项见 数据库支持,Liquibase 结构化写法见 Liquibase 数据库版本控制

4. 数据权限

详见 数据权限