约定
约定是团队的默契,为我们的工作提供一致性和可预测性。统一的规则和准则能减少沟通成本,让代码、数据库和文档更容易长期维护。
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 推荐写法:
<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 时,可按数据库分别处理:
del_flag char(1) NOT NULL DEFAULT 'F' COMMENT '删除与否',
delete_id bigint DEFAULT NULL COMMENT '删除人',
delete_time datetime DEFAULT NULL COMMENT '删除时间'del_flag char(1) NOT NULL DEFAULT 'F',
delete_id bigint,
delete_time timestampWARNING
不建议再把 Java enum 或数据库 enum 直接作为业务状态落库。跨库字段请优先使用 char / varchar / bigint 等通用类型;业务含义通过 数据字典 或 sz-module-common 中的字典常量表达,避免后续 MySQL / PostgreSQL 迁移困难。
PO 类中需要通过 @Column(isLogicDelete = true) 指定逻辑删除字段:
@Table("sys_user")
@Schema(description = "系统用户表")
public class SysUser implements Serializable {
@Column(isLogicDelete = true) // 通过 @Column 指定逻辑删除属性
private String delFlag;
// ...
}MyBatis-Flex 的逻辑删除值由各环境 mybatis-flex.yml 统一配置:
mybatis-flex:
global-config:
deleted-value-of-logic-delete: "T" # 逻辑删除字段的已删除值
normal-value-of-logic-delete: "F" # 逻辑删除字段的未删除值逻辑删除填充
框架提供逻辑删除数据填充能力。v2.0.0 的实现以实体上的 @LogicDeleteFill 为开关;只有实体标注该注解,并且表中存在注解指定的删除时间/删除人字段时,才会自动追加填充值。
@LogicDeleteFill(deleteTimeColumn = "delete_time", deleteByColumn = "delete_id")
@Table("sys_user")
public class SysUser implements Serializable {
// ...
}核心处理逻辑位于 EntityLogicDeleteListener:框架会读取实体上的 @LogicDeleteFill,使用当前数据库方言包装列名,并在逻辑删除 SQL 中追加删除时间和删除人。当前实现已将删除时间表达式统一为 CURRENT_TIMESTAMP,避免继续依赖 MySQL 风格的 now(),更适合 MySQL / PostgreSQL 以及多数据源场景。
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_id | createId | Long |
create_time | createTime | LocalDateTime |
update_id | updateId | Long |
update_time | updateTime | LocalDateTime |
Liquibase changelog 推荐写法:
<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 示例:
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 '更新人'create_time timestamp DEFAULT CURRENT_TIMESTAMP,
update_time timestamp DEFAULT CURRENT_TIMESTAMP,
create_id bigint,
update_id bigintPO 类需要在 @Table 中启用监听器:
@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 会在新增时填充 createId、createTime,在更新时填充 updateId、updateTime。如果实体中不存在对应属性,监听器会忽略该字段并记录日志。
3. SQL 的编写
框架集成了 MyBatis-Flex,并在 v2.0.0 补齐了 MySQL / PostgreSQL 双数据库支持。因此 SQL 编写不再只考虑 MySQL。
推荐优先级如下:
- 普通查询优先使用 MyBatis-Flex QueryWrapper / QueryChain,减少手写方言 SQL。
- 复杂动态查询或性能敏感查询可以放到 Mapper XML。若模块需要同时兼容 MySQL 和 PostgreSQL,应避免
GROUP_CONCAT、FIND_IN_SET、INSERT IGNORE、反引号、MySQLenum等单库写法;若项目已固定目标数据库,可按目标库能力编写,但建议在文件或方法注释中说明数据库前提。 - 初始化数据、菜单、字典、角色权限等脚本优先使用 Liquibase XML 的结构化标签,例如
<createTable>、<insert>、<column>,不要优先写裸 SQL 或 CDATA。 - 如果确实存在数据库方言差异,固定单库项目可按目标库维护;需要双库兼容时,应在 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. 数据权限
详见 数据权限。
