Skip to content

数据库约定与表总览 ​

适用端:后端 | 类型:参考

MyBatis-Plus + dynamic-datasource,MySQL 为主、九种方言可选。本页讲清公共字段、逻辑删除、多租户、主键策略与类型处理器这五件事,以及全库 229 张表的分布。

前置阅读:接口约定与 CRUD 骨架。

它解决什么问题 ​

每张业务表都有 6 个「不是业务字段但必须存在」的列,每个查询都要自动带上租户与逻辑删除条件。这些都不该由业务代码手写:

代码在哪 ​

关注点类 / 文件
公共字段基类enlev-spring-boot-starter-mybatis/.../mybatis/core/dataobject/BaseDO.java
多租户基类enlev-spring-boot-starter-biz-tenant/.../tenant/core/db/TenantBaseDO.java
字段自动填充enlev-spring-boot-starter-mybatis/.../mybatis/core/handler/DefaultDBFieldHandler.java
MyBatis 自动配置.../mybatis/config/EnlevMybatisAutoConfiguration.java(@MapperScan("${enlev.info.base-package}", annotationClass = Mapper.class))
租户拦截器.../tenant/config/EnlevTenantAutoConfiguration.java(new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties)))
主键策略适配.../mybatis/core/IdTypeEnvironmentPostProcessor.java
Mapper 基类.../mybatis/core/mapper/BaseMapperX.java、.../core/query/LambdaQueryWrapperX.java
类型处理器.../mybatis/core/type/{LongListTypeHandler, IntegerListTypeHandler, LongSetTypeHandler, StringListTypeHandler, EncryptTypeHandler}.java
全局配置enlev-server/src/main/resources/application.yaml 的 mybatis-plus: 与 mybatis-plus-join: 段

核心机制 ​

六个公共字段 ​

java
// BaseDO(abstract,实现 Serializable 与 Easy-Trans 的 TransPojo)
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

@TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
private String creator;

@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
private String updater;

@TableLogic
private Boolean deleted;

TenantBaseDO extends BaseDO 只多一个字段:private Long tenantId;。

creator / updater 存的是用户 id 的字符串形式,由 DefaultDBFieldHandler 在填充时取 SecurityFrameworkUtils.getLoginUserId()。所以:

  • 无登录上下文的写入(定时任务、匿名 webhook)里这两列是 null;
  • 列表页显示「创建人昵称」需要翻译,前端用 useUserLabel,后端用 Easy-Trans(starter-mybatis 的 EnlevTranslateAutoConfiguration)。

逻辑删除 ​

@TableLogic + yaml 里的值定义:

yaml
mybatis-plus:
  global-config:
    db-config:
      logic-delete-value: 1
      logic-not-delete-value: 0
  type-aliases-package: ${enlev.info.base-package}.module.*.dal.dataobject

deleteById 实际执行 UPDATE ... SET deleted = 1,所有查询自动追加 deleted = 0。要查已删数据只能写原生 SQL 或用 @InterceptorIgnore,LambdaQueryWrapperX 绕不过去。

唯一性校验要考虑逻辑删除

deleted 只是查询条件,不是唯一索引的一部分。validatePostNameUnique 这类「同名校验」查不到已删行,于是「删掉再新建同名」会通过校验,但如果表上有 UNIQUE(name),插入会被数据库拒绝并报 500。要么唯一索引带上 deleted,要么校验时显式查全量。

多租户 ​

enlev.tenant.enable: true,拦截器给除白名单外的所有表自动拼 tenant_id。白名单在 yaml 里分三类:

yaml
enlev:
  tenant:
    enable: true
    ignore-urls:
      - /jmreport/* # 积木报表,无法携带租户编号
    ignore-visit-urls:
      - /admin-api/system/user/profile/**
      - /admin-api/system/auth/**
    ignore-tables:
      # 模型单价是平台与厂商之间的事实,与租户无关 —— 同一个模型不会因为换个租户就换个进价。
      # 不登记在这里的话,租户拦截器会给它拼 tenant_id,而该表没有这一列,
      # 表现是「Unknown column 'tenant_id'」并且【把整条计量记录一起丢掉】
      - agent_model_price
    ignore-caches:
      - user_role_ids
      - permission_menu_ids
      - oauth_client
      - notify_template
      # … mail_account / mail_template / sms_template / iot:device / iot:thing_model_list

实测全库 229 张表中 112 张有 tenant_id 列。判断一张新表要不要租户化:

情况做法
租户私有业务数据建表带 tenant_id bigint NOT NULL DEFAULT '0'。DO 继承 BaseDO 就够,只有代码里需要读写 tenantId 时才继承 TenantBaseDO
平台级事实(单价、字典模板、内置定义)建表不带 tenant_id,并把表名加进 ignore-tables
平台级缓存加进 ignore-caches,否则缓存 key 会带租户导致命中率异常

是不是租户表由「有没有这一列」决定,不由 DO 基类决定

TenantLineInnerInterceptor 工作在 SQL 层:它按表名判断是否拼 AND tenant_id = ?,根本不看实体有没有 tenantId 字段。实测全仓 97 个 DO 继承 BaseDO、只有 30 个继承 TenantBaseDO,而带 tenant_id 列的表有 112 张——差额就是这么来的。

一个现成例子:system_post 有 tenant_id 列,但 PostDO extends BaseDO。岗位照常按租户隔离,代码里只是从来不需要读这个值。

忘了登记 ignore-tables 的失败方式很安静

如上注释所说:拼了不存在的列 → SQL 报错 → 在计量这类「写日志」路径上,异常被吞掉的结果是整条记录丢失,不是接口报错。新增平台级表时,ignore-tables 与建表语句要同一次提交。

代码里临时忽略租户用 @TenantIgnore(starter-biz-tenant 的 tenant/core/aop/,支持 SpEL 的 enable())。

主键策略:id-type: NONE 的智能模式 ​

yaml
mybatis-plus:
  global-config:
    db-config:
      id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。

IdTypeEnvironmentPostProcessor 在启动时按数据源方言决定:MySQL / 达梦 → IdType.AUTO;Oracle / PostgreSQL / Kingbase / DB2 / H2 → IdType.INPUT,并注册对应的 keyGenerator bean(PostgreKeyGenerator / OracleKeyGenerator 等),配合 DO 上的 @KeySequence("xxx_seq") 取号。

所以 DO 上那句看着多余的注解是有用的:

java
@TableName("system_post")
@KeySequence("system_post_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。

代码生成器会自动带上它,手写 DO 时照抄即可;换成 ASSIGN_ID(雪花)则必须把 @KeySequence 去掉(yaml 注释已说明)。

单元测试跑在 H2 上,走的正是 INPUT 分支——这也是为什么 DO 缺 @KeySequence 时 MySQL 下正常、单测里却插入失败。

查询与分页 ​

BaseMapperX 在 MP 的 BaseMapper 之上补了 selectPage(PageParam, Wrapper)、selectOne/selectList 的多参重载、deleteByIds 等;LambdaQueryWrapperX 补了 eqIfPresent / likeIfPresent / inIfPresent / betweenIfPresent 系列(参数为空则不加条件)。分页拦截器 PaginationInnerInterceptor 与乐观锁 OptimisticLockerInnerInterceptor 在 EnlevMybatisAutoConfiguration 里注册(乐观锁只对带 @Version 的字段生效)。

连表查询用 mybatis-plus-join,其全局配置也值得知道:

yaml
mybatis-plus-join:
  sub-table-logic: true # 全局启用副表逻辑删除
  table-alias: t        # 表别名(默认 t)
  logic-del-type: on    # 副表逻辑删除条件放在 ON 而不是 WHERE

类型处理器 ​

处理器用途Java 类型 ↔ 列
JacksonTypeHandler(MP 自带)JSON 对象Object / 自定义类 ↔ json
LongListTypeHandler逗号分隔的 id 串List<Long> ↔ varchar
IntegerListTypeHandler / LongSetTypeHandler / StringListTypeHandler同上List<Integer> / Set<Long> / List<String>
EncryptTypeHandler字段级 AES 加密String ↔ varchar(密文)

用法(真实例子):

java
// AdminUserDO.java:85 —— JSON 字段(岗位编号数组)
@TableField(typeHandler = JacksonTypeHandler.class)
private Set<Long> postIds;

// MailLogDO.java:56 —— 字符串列表(接收邮箱地址)
@TableField(typeHandler = StringListTypeHandler.class)
private List<String> toMails;

EncryptTypeHandler 的密钥在 yaml:mybatis-plus.encryptor.password。改了密钥等于已加密的数据全部解不开,且不会有明显报错(表现为字段乱码或空)。

数据模型 ​

全库 212 张表(实测 information_schema,库 ainb-professional-init,2026-10-25),按前缀分布:

前缀张数归属
act_39Flowable 工作流引擎(bpm 域)
test_39代码生成器的示例表与联调残留
system_34上游系统域(用户/角色/菜单/租户/OAuth2/短信/邮件)+ ★ system_nav_node(分端导航树)
infra_24上游基础设施 + ★ 低代码定制 13 张(infra_data_model{,_log}、infra_form_design{,_data_log,_menu,_version}、infra_portal{,_version}、infra_data_interface{,_log,_version}、infra_bill_template{,_log})
agent_16★ 自研 Agent 域
ai_13AI 域
bpm_12工作流业务表
qrtz_11Quartz
biz_9低代码建出来的业务表(前缀由 enlev.data-model.table-prefix-enforced 强制)
sys_7★ 定制权限体系(sys_resource / sys_grant / sys_rule / sys_subject / sys_subject_member / sys_slot_mapping / sys_record_share)
flw_6Flowable 事件注册
graph_2spring-ai-alibaba graph

本页上一版写的是「229 张表,含 im_ 17 张」,那两个数都已经不成立

im_ 前缀的表在 ainb-professional-init 与 ruoyi-vue-pro 里都是 0 张(enlev-module-im 已随 12347efcd1 连目录一起删除,表也跟着清掉了)。229 → 212 的差额主要就是这 17 张。旧库 ruoyi-vue-pro 现在是 211 张:与 212 的唯一差别就是少一张 system_nav_node,它已停在 nav-node 特性之前,不要再拿它当事实来源。

表结构不要手抄

定制域 36 张表(权限 7 + 低代码 13 + agent 16)的逐字段结构由 tools/gen-tables.mjs 从 information_schema 生成到文档工程的 docs/.generated/tables-{index,permission,lowcode,agent}.md,含字段类型、约束、键、注释与索引清单。写文档、对字段一律引用生成物。

各方言建表脚本在 ainb/sql/:mysql / oracle / postgresql / dm / kingbase / sqlserver / highgo / opengauss / db2,另有 sql/tools/convertor.py(MySQL → 其它库转换)与 sql/tools/docker-compose.yaml(各方言容器)。

怎么扩展 ​

新建一张业务表的最小清单:

  1. 建表:表名 <模块前缀>_<业务名>(小写下划线);必备列 id bigint NOT NULL AUTO_INCREMENT、creator varchar(64)、create_time datetime NOT NULL、updater varchar(64)、update_time datetime NOT NULL、deleted bit(1) NOT NULL DEFAULT 0;租户化再加 tenant_id bigint NOT NULL DEFAULT '0'(实测 94 张带 tenant_id 列的表里只有 26 张给它建了索引,system_post 就只有 PRIMARY——要不要加索引按查询量自己判断,不是硬约定);每列都要写 COMMENT(代码生成器与低代码建模都读注释);
  2. DO:@TableName + @KeySequence + 继承 BaseDO 或 TenantBaseDO;
  3. Mapper:extends BaseMapperX<XxxDO> + default selectPage;
  4. 若走低代码建模(infra_data_model),建表由平台的 DDL 通道执行,受 SqlSafeGuard 与 enlev.data-model.* 约束(保留字、标识符白名单、框架表黑名单是硬编码不可关的),别手工绕过;
  5. 若是平台级表,同步在 enlev.tenant.ignore-tables 登记。

坑与注意 ​

  • sql/mysql/ 下的两份全量快照都不能单独用:ainb-professional-init.sql(39 MB,211 张表)平台表齐全但没有 system_nav_node;ainb-init.sql(13 MB,95 张表)有 system_nav_node 却缺 64 张代码声明的表。正确路径是「导专业版快照 → 按依赖顺序跑 sql/mysql/update/ 那 6 个增量脚本 → 重启后端」,详见环境准备与启动。没有 Flyway / Liquibase,增量脚本要人工跑。上游基线在 ruoyi-vue-pro.sql(48 张建表语句),只作对照。
  • deleted 是 bit(1),Java 侧是 Boolean。手写 SQL 时用 deleted = 0 / = 1,别写 = false(部分方言不认)。
  • H2 单测需要 src/test/resources/sql/{create_tables.sql, clean.sql},目前只有 bpm / im / infra / system 四个模块有。在 ai / agent 模块加 DB 测试要先补这两份文件和 application-unit-test.yaml。
  • update_time 是 NOT NULL,但 updater 允许 null;反过来 create_time 也 NOT NULL。手工插数据(脚本、导入)时漏掉这两列会直接失败。
  • creator / updater 存 id 不存名字,改名不影响历史数据;但直接查库看到的是数字,排障时容易误判成「同一个人」。
  • mybatis-plus.configuration.map-underscore-to-camel-case: true 是显式写的(虽然默认就是 true)。DO 字段名与列名不一致时用 @TableField("列名") 显式指定,别依赖驼峰转换去猜。
  • 数据源是 dynamic-datasource,primary: master,slave 配了 lazy: true(同库同账号)。用 @DS("slave") 切库前要确认 slave 真的指向你想读的地方——local profile 下它就是同一个库,读写分离问题在本地复现不出来。

→ 本节是贴着上下文的提醒;按症状查的全域清单见坑位索引。

相关页面 ​