跳到内容

Database Starter

受众:开发者 摘要:MyBatis-Plus + Liquibase 关系型数据库底座。BaseEntity / BaseService / 结构化查询 / 树形组装 / 多数据库迁移 / 乐观锁 / 逻辑删除。

何时使用

场景建议
关系型业务数据持久化BaseEntity + BaseServiceImpl + Mapper
分页 / 条件搜索 / 排序CrudQuery + CrudQuerySpec 白名单
字段名解析(白名单、排序、导出)LambdaField
内存组树(菜单 / 部门 / 分类)TreeSpec
数据库版本管理Liquibase XML 迁移文件
跨实例迁移并发安全内置 liquibase-sessionlock
乐观锁@Version + optimistic_lock_version
逻辑删除@TableLogic + deleted_at
缓存 / 登录态 / 队列等 Key-Valueredis.md

快速开始

xml
<dependency>
    <groupId>com.springopen</groupId>
    <artifactId>spring-open-starter-database</artifactId>
</dependency>

数据源(MySQL):

dotenv
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=spring_open
DB_USERNAME=spring_open
DB_PASSWORD=spring_open

实体:

java
@TableName("example_item")
public class ExampleItem extends BaseEntity {
    @TableField("item_code")
    private String itemCode;
}

Service:

java
@Service
public class ExampleItemServiceImpl
        extends BaseServiceImpl<ExampleItemMapper, ExampleItem>
        implements ExampleItemService {
}

Mapper:

java
public interface ExampleItemMapper extends BaseMapper<ExampleItem> { }

Mapper 扫描由 spring-open-starter-database 统一提供,业务模块只需要声明继承 BaseMapper<T> 的 Mapper 接口,不需要再写局部 @MapperScan

支持的数据库

数据库等级
MySQL 5.7+默认
PostgreSQL 15+一等支持
H2 2.xlite 兼容 / 隔离诊断模式,作普通开发或生产

迁移文件按 MySQL 5.7+ / PostgreSQL 15+ / H2 2.x 兼容方向编写,依赖 MySQL 8 专属语法。

配置项

yaml
spring:
  datasource:
    type: ${DB_TYPE:com.zaxxer.hikari.HikariDataSource}
    url: ${DB_URL:jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_DATABASE:spring_open}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai}
    username: ${DB_USERNAME:spring_open}
    password: ${DB_PASSWORD:spring_open}
    driver-class-name: ${DB_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver}

mybatis-plus:
  global-config:
    banner: false

spring:
  open:
    data:
      mybatis-plus:
        enabled: true
        db-type: auto
        pagination-enabled: true
        pagination-max-limit: 500
        pagination-overflow: false
        optimistic-locker-enabled: true
        block-attack-enabled: true
        auto-fill-enabled: true
        model-events-enabled: true
配置默认值说明
enabledtrue启用开关
db-typeauto分页插件 DB 类型,按 URL 推断
pagination-enabledtrue分页插件
pagination-max-limit空(应用装配 500单页最大条数,空表示不额外限制
pagination-overflowfalse超出总页数是否回到第一页
optimistic-locker-enabledtrue乐观锁插件
block-attack-enabledtrue全表 update/delete 防护
auto-fill-enabledtrue审计时间自动填充
model-events-enabledtrue模型生命周期事件(ModelObserver + ModelEvent

切换 PostgreSQL:

dotenv
DB_URL=jdbc:postgresql://127.0.0.1:5432/spring_open
DB_DRIVER_CLASS_NAME=org.postgresql.Driver

lite 兼容模式 H2 文件:

text
${APP_STORAGE_DIR:${user.dir}/storage}/database/${DB_DATABASE:spring_open}.db
bash
./mvnw -Plite -pl spring-open-application spring-boot:run

storage/ 是私有持久化目录;H2 大小写参数(DATABASE_TO_UPPER)是数据库创建期语义,在新旧 URL 间来回切换。

源码机制骨架

机制源码说明
自动配置DataMybatisPlusAutoConfiguration在 MyBatis-Plus 自动配置之前注册插件链、审计填充器和 BaseMapper 扫描规则
配置属性DataMybatisPlusProperties承接 spring.open.data.mybatis-plus.* 开关和分页数据库类型推断
基础实体BaseEntity统一主键、审计时间、审计用户、乐观锁和逻辑删除字段
审计填充DataMybatisPlusMetaObjectHandler插入时填充 createdAt / updatedAt,更新时刷新 updatedAt
基础 ServiceBaseService / BaseServiceImpl提供分页、必取、创建、更新、删除、批量删除和结构化查询闭环
结构化查询CrudQuery / CrudQuerySpec / SearchCondition / SortCondition用 Service 侧白名单把外部字段和操作符转换为安全的 MyBatis-Plus Wrapper
字段解析LambdaField / FieldName从实体 getter 解析 Java 属性名和数据库列名,优先尊重 MyBatis-Plus 表元信息
树形组装TreeSpec在内存中按 id / parentId 组装树,查询和权限过滤由调用方负责

扩展边界

  • Database starter 只提供 MyBatis-Plus 装配、Mapper 扫描、基础 Entity / Service、结构化查询、字段解析和树形组装。
  • Database starter 不提供 Provider 切换;数据库类型由 spring.datasource.* 和连接池配置决定,分页插件数据库类型可通过 db-type 显式指定。
  • 业务表结构和 Liquibase 迁移归各业务模块维护;starter 不反向拥有业务 schema。
  • BaseServiceImpl 生命周期钩子只用于领域前后置处理,不承接权限、数据范围、余额扣减、支付补偿或跨模块事务编排。
  • CrudQuerySpec 是查询白名单边界。Controller 和前端不能直接透传数据库列名、排序列或 SQL 片段。
  • 租户、组织、状态机、所有权等字段不进入 BaseEntity,由业务实体按自身上下文声明。
  • 多对多关联重分配不要依赖逻辑删除旧关系后再插入同一唯一键组合,详见“关联表唯一键”。

Facade / 基类 API

BaseEntity

字段类型说明
idLong主键,IdType.AUTO
createdAt / updatedAtLocalDateTime自动填充
createdBy / updatedByLong业务 Service 填充
optimisticLockVersionInteger乐观锁版本号
deletedAtLocalDateTime逻辑删除

暴露 ID 泛型;主线 Long 主键,Service ID 用 Serializable

租户字段BaseEntity。确实多租户的实体自己声明:

java
public class Order extends BaseEntity {
    @TableField("tenant_id")
    private Long tenantId;
}

BaseService

方法说明
page(PageQuery query)分页查询
page(PageQuery query, Wrapper<T> wrapper)按条件分页
page(CrudQuery query, CrudQuerySpec<T> spec)按结构化查询分页
list(CrudQuery query, CrudQuerySpec<T> spec)按结构化查询列表
getRequired(Serializable id)不存在抛异常
create(T entity)创建
update(Serializable id, T entity)修改
deleteRequired(Serializable id)删除
deleteBatchRequired(Collection<?> ids)批量删除

BaseServiceImpl 加写事务并提供生命周期钩子:

java
protected T beforeCreate(T entity);
protected T afterCreate(T entity);
protected T beforeUpdate(T entity);
protected T afterUpdate(T entity);
protected void beforeDelete(Serializable id, T entity);
protected void afterDelete(Serializable id, T entity);
protected void beforeBatchDelete(Collection<?> ids);
protected void afterBatchDelete(Collection<?> ids);

通用基类保持轻量。塞导入导出、树结构、文件等可选能力。

上面的 beforeX / afterX 钩子是单个 Service 自身的前后置处理。跨模块、跨 Service 的横切(审计、缓存失效、Outbox)请用下面的模型事件,别在业务 Service 里手写。

模型生命周期事件

BaseServicecreate / update / deleteRequired / deleteBatchRequired 写入闭环会触发模型生命周期事件(对标 Laravel Eloquent 的 creating/created/updating/updated/deleting/deleted),把审计、缓存失效、Outbox 投递等横切从业务 Service 解耦出来。两种消费方式:

1. 类型化观察者(对标 Eloquent Observer)——声明为 Spring Bean、泛型指定实体类型即自动生效,无需业务 Service 挂钩:

java
@Component
public class OrderObserver implements ModelObserver<Order> {
    @Override public void created(Order order) { /* 发通知 / 建索引 */ }
    @Override public void updated(Order order) { /* 失效缓存 */ }
    @Override public void deleting(Order order, Object id) { /* 前置校验,抛异常可阻断并回滚 */ }
}

以父实体类型声明可覆盖其所有子类;*ing 前置钩子与写入同事务,抛异常回滚本次写入。

2. Spring 应用事件——同一次写入也会发布 ModelEvent<T>,可用 @EventListener / @TransactionalEventListener 消费(后者拿提交后语义);ModelEvent<T> 携带泛型,可用 ModelEvent<Order> 精确订阅:

java
@TransactionalEventListener(phase = AFTER_COMMIT)
public void onOrder(ModelEvent<Order> event) {
    if (event.lifecycle() == ModelLifecycle.CREATED) { /* 提交后异步 */ }
}

事件在写入前(*ING,主键可能未回填)与写入成功后(*ED,仍在事务内)触发。批量删除按每个主键各发一次 DELETING / DELETED(实体为 null、仅带主键)。仅覆盖走 BaseService 的写入;Mapper 直连的裸 SQL 不触发(与 Laravel 模型事件不覆盖 DB::table() 一致)。关闭:spring.open.data.mybatis-plus.model-events-enabled=false

Provider

Database 本身提供 Provider 切换;数据库类型通过 spring.datasource.url 推断。扩展能力:

  • 自定义 MetaObjectHandler 改自动填充
  • 自定义 ISqlInjector 替换 MyBatis-Plus 默认注入器
  • 通过 Spring Boot / MyBatis-Plus 原生配置项进一步定制
  • 数据源连接池可通过 spring.datasource.type 切换(Hikari / Druid 等)

结构化查询

Service-first 能力,提供万能通用 Controller。

核心类型:

类型说明
CrudQuery分页 + 搜索条件 + 排序
SearchCondition单字段或多字段条件
SortCondition排序条件
SearchOperatorEQ / LIKE / IN / BETWEEN / IS_NULL
CrudQuerySpec<T>字段白名单 + operator 白名单 + 排序白名单 + 默认排序
java
private static final CrudQuerySpec<ExampleItem> QUERY_SPEC = CrudQuerySpec.<ExampleItem>builder()
        .sortableField(ExampleItem::getCode, SearchOperator.EQ, SearchOperator.LIKE)
        .sortableField(ExampleItem::getCreatedAt, SearchOperator.GE, SearchOperator.LE)
        .field(ExampleItem::getDeletedAt, SearchOperator.IS_NULL, SearchOperator.IS_NOT_NULL)
        .defaultSort(ExampleItem::getUpdatedAt, SortCondition.Direction.DESC)
        .build();

lambda 同时解析 Java 属性名和数据库列名。createdAtcreated_at 都归到同一白名单字段。对外 API 推荐用 Java 属性名;列名是兼容别名。

虚拟字段、关联字段、聚合字段或 DTO 别名用字符串 API:

java
CrudQuerySpec.<Order>builder()
        .sortableField("ownerName", "u.owner_name", SearchOperator.LIKE)
        .build();

只允许排序、不允许查询的字段用 sortable(例如可按时间排序但不开放过滤的列):

java
CrudQuerySpec.<Order>builder()
        .sortable(Order::getCreatedAt)
        .build();

所有字段和操作符必须进白名单。未声明字段、未允许 operator、非法排序字段抛统一参数异常。 SearchCondition 提供 like / likeAny / in / notIn / between / isNull / isNotNull 工厂方法;更复杂的条件仍应先在 Service 层映射到白名单字段。

LambdaField

字段解析工具,适合白名单、排序、导出、审计、动态查询和表格列复用。

java
String propertyName = LambdaField.propertyName(ExampleItem::getDeletedAt);  // deletedAt
String columnName   = LambdaField.columnName(ExampleItem::getDeletedAt);    // deleted_at
FieldName fieldName = LambdaField.of(ExampleItem::getDeletedAt);

尊重 MyBatis-Plus 注解:

java
@TableId("entity_id") private Long id;
@TableField("created_on") private LocalDateTime createdAt;

LambdaField.columnName(Entity::getId);         // entity_id
LambdaField.columnName(Entity::getCreatedAt);  // created_on

LambdaField 只解析字段名,代表开放查询权限。对外查询仍走 CrudQuerySpec 白名单。

TreeSpec

轻量树形组装规则,菜单 / 部门 / 配置分类等已查列表按 id / parentId 组装树:

java
TreeSpec<MenuVO> spec = TreeSpec.<MenuVO>builder()
        .id(MenuVO::getId)
        .parentId(MenuVO::getParentId)
        .children(MenuVO::setChildren)
        .sort(Comparator.comparing(MenuVO::getSortOrder))
        .build();

List<MenuVO> tree = spec.build(list);

默认根节点:parentId == null / 0 / "0" / 父节点不在当前列表。

特殊根节点:

java
TreeSpec.<MenuVO>builder()
        .rootParentId(-1L)
        ...
        .build();

树形接口保留显式 Controller 和显式 Service 方法(如 /api/v1/admin/example-categories/tree)。因有 TreeSpec 就引入万能 Tree Controller。

Mapper

优先继承 MyBatis-Plus BaseMapper<T>

java
public interface ExampleItemMapper extends BaseMapper<ExampleItem> { }

普通 CRUD 用 MyBatis-Plus API,手写 SQL。

手写 SQL 仅限:

  • 复杂聚合统计
  • 多表关联且 wrapper 难表达
  • 数据库或第三方组件表的只读运维查询

要求:

  • 保持 MySQL / PostgreSQL / H2 兼容
  • SQL 集中在 Mapper 或 XML,散落 Service 字符串拼接
  • 表名、列名来自用户请求
  • 数据库差异化写法在文档或测试说明

Liquibase 迁移

主入口:

text
spring-open-application/src/main/resources/db/changelog/db.changelog-master.xml

模块迁移文件:

text
spring-open-core/spring-open-core-iam/src/main/resources/db/changelog/migrations/
  0000_00_00_010000_core_iam_create_tables.xml
  0000_00_00_010100_core_iam_seed_data.xml
spring-open-core/spring-open-core-config/src/main/resources/db/changelog/migrations/
  0000_00_00_011000_core_config_create_tables.xml
  0000_00_00_011100_core_config_seed_data.xml

changeSet ID 命名

迁移文件采用 Laravel 风格可排序前缀。初始化基线推荐格式:

text
0000_00_00_<6位序号>_<owner>_<action>.xml

后续增量迁移推荐使用真实日期时间:

text
<yyyy_MM_dd_HHmmss>_<owner>_<action>.xml

changeSet id 推荐格式:

text
<排序前缀>.<3位序号>.<owner-hyphen>-<action>

示例:

xml
<!-- 文件:0000_00_00_401000_module_cms_create_tables.xml -->
<changeSet id="0000_00_00_401000.001.module-cms-create-cms-content" author="system">
    <!-- ... -->
</changeSet>

<!-- 文件:0000_00_00_401100_module_cms_seed_data.xml -->
<changeSet id="0000_00_00_401100.001.module-cms-iam-permission" author="system">
    <!-- ... -->
</changeSet>

约定:

  • 排序前缀表示全仓主迁移文件顺序,必须全仓唯一,并按实际执行顺序递增,用来稳定 includeAll 的跨文件执行顺序
  • owner 从物理模块路径推导,使用 starter_* / core_* / module_*,多词模块用下划线连接;changeSet id 中的 owner 使用短横线形式,例如 module-cms
  • 模块 schema 与 seed 分文件维护:建表、字段、索引、约束放模块主迁移文件;初始化数据、权限、菜单、角色绑定和当前基线数据修正放对应模块目录下的 *_seed_data.xml 或语义明确的 seed 文件,例如 0000_00_00_010000_core_iam_create_tables.xml + 0000_00_00_010100_core_iam_seed_data.xml
  • 只要模块存在数据库实体,就必须同时存在模块 schema .xml 与同模块 seed .xml;seed 文件允许使用 insert / loadData / loadUpdateData / update / delete 等数据类 changeSet,但不得包含 createTable / addColumn / createIndex / renameTable / dropTable 等 schema changeSet,也不得保留旧库兼容式 preConditions、checksum 或可变执行标记
  • 第一版初始化基线使用 0000_00_00_<6位序号>,按模块依赖拓扑分区:starter 000000-009999、core 010000-399999、module 400000-799999、install / application 800000-899999、test / reserved 900000-999999;每个 owner 默认预留 1000 序号,owner 内 schema 用块起点,seed 用 +100,补充基线按 +200 / +300 递增
  • 当前开发期保护已迁移文件:只要某迁移文件可能已经在本地库、共享库或协作环境执行过,就不得为了补洞、压实、改 author、重排或折叠而回改该文件;新增表、字段、索引、约束、菜单或基线数据修正使用真实日期时间前缀新增增量迁移
  • 全新模块尚未执行的迁移草稿,可以在首次执行前受控整理、折叠或调整依赖顺序,并同步更新文档、测试和所有旧文件名引用
  • V1.0 发布前会单独做一次基线迁移整理 / freeze,把开发期已验证的增量按维护者确认重新整理为发布基线;该工作不作为日常开发变更处理
  • 3位序号 表示当前迁移文件内的 changeSet 执行顺序,例如 001 / 002;必须和文件内实际出现顺序一致
  • action 使用英文动作短语,例如 create-tables / add-column / create-index / iam-permission
  • 新增或尚未执行的 author 统一使用中性值 system,不使用项目名、组织名或个人名;已发布或已执行的 changeSet 不为统一 author 而改动,author 作为 Liquibase 身份必须保持稳定
  • 已发布或已执行的 changeSet 不为改名而改名;需要修订时新增 changeSet,不通过清库重跑来掩盖迁移身份或顺序问题

author 是 Liquibase changeSet 身份的一部分,和 filenameid 一起决定迁移是否已经执行。开发期如果发现本地库已经执行过旧 author 的 changeSet,优先保持主迁移里的旧身份稳定;只有明确要让旧开发库跟随统一 author 时,才先备份并同步更新 database_changelog.author 元数据。不要通过删除 changelog 表、只刷新 checksum,或给建表迁移追加跳过式 precondition 来掩盖身份不一致。具体处理步骤见 升级指南

提交前建议执行:

bash
./.ai/scripts/check migrations

优先 Liquibase 标准标签

xml
<createTable tableName="example_item">
    <column name="id" type="bigint" autoIncrement="true">
        <constraints primaryKey="true" nullable="false"/>
    </column>
    <column name="name" type="varchar(100)">
        <constraints nullable="false"/>
    </column>
    <column name="created_at" type="datetime" defaultValueComputed="CURRENT_TIMESTAMP">
        <constraints nullable="false"/>
    </column>
    <column name="updated_at" type="datetime" defaultValueComputed="CURRENT_TIMESTAMP">
        <constraints nullable="false"/>
    </column>
    <column name="optimistic_lock_version" type="int" defaultValueNumeric="0">
        <constraints nullable="false"/>
    </column>
    <column name="deleted_at" type="datetime"/>
</createTable>

种子数据:

xml
<insert tableName="iam_permission">
    <column name="code" value="example:item:view"/>
    <column name="name" value="查看示例"/>
    <column name="created_at" valueComputed="CURRENT_TIMESTAMP"/>
    <column name="updated_at" valueComputed="CURRENT_TIMESTAMP"/>
</insert>

引用其他表 ID:

xml
<column name="role_id" valueComputed="(SELECT id FROM iam_role WHERE code = 'super-admin')"/>

避免固定数据库 SQL

默认写死 MySQL SQL(ON DUPLICATE KEY UPDATE 等)。优先 <createTable> / <addColumn> / <createIndex> / <addUniqueConstraint> / <insert> / <preConditions> / valueComputed

只有标准标签无法表达且明确兼容风险时才用 <sql>

内置会话锁

Database starter 内置 Liquibase 运行时和 liquibase-sessionlock。应用执行迁移用数据库会话级锁;进程中断后连接断开锁自动释放,减少 database_changelog_lock 残留导致的启动阻塞。

主要覆盖 MySQL / MariaDB / PostgreSQL / Oracle。lite H2 模式仍避免迁移过程强制终止应用。

字段约定

数据库 snake_case,Java camelCase:

text
created_at    → createdAt
updated_at    → updatedAt
created_by    → createdBy
updated_by    → updatedBy
optimistic_lock_version → optimisticLockVersion
deleted_at    → deletedAt

逻辑删除

java
@TableLogic(value = "null", delval = "now()")
@TableField(value = "deleted_at", fill = FieldFill.UPDATE)
private LocalDateTime deletedAt;

后台查询显式过滤:

java
.isNull(Entity::getDeletedAt)

把逻辑删除表当物理删除使用。审计、支付、通知、队列等运维数据按业务做受控清理。

关联表唯一键

多对多关联表如果同时具备组合唯一键和逻辑删除字段,不能用“逻辑删除旧关系后重新插入同一关系”实现重分配,因为软删除行仍会占用唯一键。例如 user_id + role_idrole_id + menu_idrole_id + permission_iduser_id + post_id 这类关系表,重新分配一组关系前应物理清理当前主体的旧关联,或恢复 / 更新既有行后继续使用。

典型场景:

  • 用户重新分配角色。
  • 角色重新分配菜单。
  • 角色重新分配权限。
  • 用户重新分配岗位。

这类表通常只表达“当前关系集合”,审计应通过操作日志或专门审计表记录,不依赖软删除行保留历史关系。

乐观锁

java
@Version
@TableField("optimistic_lock_version")
private Integer optimisticLockVersion;

适合用户、配置、角色、菜单、支付订单等并发修改资源。

审计字段

createdAt / updatedAtDataMybatisPlusMetaObjectHandler 自动填充;createdBy / updatedBy 业务 Service 填:

java
Long operatorId = Auth.id(Long.class);
entity.setUpdatedBy(operatorId);

新增数据库资源规范

新增与数据库表交互的实体类时同步补:

  • Liquibase 迁移
  • Entity / Mapper / DTO / VO / Service / Controller
  • RBAC 权限码
  • 必要测试
  • 正式文档或模块文档更新

标准后台接口:

text
GET    /resources
GET    /resources/{id}
POST   /resources
PUT    /resources/{id}
DELETE /resources/{id}
DELETE /resources          # 批量删除,ID 放请求体

DTO / VO / Entity

  • Controller 入参 DTO + Validation
  • Controller 出参 VO
  • Entity 直接作接口响应
  • entity 包只放数据库表映射实体;状态、类型、分类、消息 key、字段 key、转换器、校验器等辅助类放 support 或语义更明确的包
  • Service 接收 command/query 对象或明确参数
  • Controller 写业务逻辑

目录:

text
example
├── controller
├── dto
├── entity
├── mapper
├── service
├── support
└── vo

分页

入参用 PageQuery 或继承它的 DTO;返回 PageResult<T>

java
return ResponseEntity.ok(Result.success(page));

多数据库兼容建议

  • 表名 / 字段名小写 snake_case
  • 用数据库保留字
  • 字段类型优先使用 Liquibase 通用类型:bigint / int / boolean / datetime / date / decimal(p,s) / varchar(n) / text
  • 时间字段用 datetime,只有明确日期语义才用 date
  • 大文本用 text为 MySQL 写死 longtext,也不使用兼容性较差的 clob
  • 不使用方言化类型承载普通业务字段,例如 jsonbdoubletimestamp(6)uuidtinyint
  • 二进制或扩展类型只允许在组件边界显式例外(db-scheduler task_data MySQL blob / PostgreSQL bytea,可选 pgvector vector(256)
  • 业务 payload / 队列 payload / 通知内容等可读数据优先 text,便于导出迁移
  • 普通 CRUD 用 MyBatis-Plus,业务代码拼接 SQL

开发约定

  • 实体继承 BaseEntity;多租户字段实体自声明
  • Service 继承 BaseServiceImpl,钩子里做业务校验
  • Controller 用 BaseController + 显式 DTO / VO
  • 结构化查询用 CrudQuerySpec 白名单,禁止前端字段直接拼 SQL
  • 迁移文件每个 <changeSet> 唯一 ID + 唯一 author
  • 同一迁移事后修改,需要修订时新增 changeSet
  • 涉及生产数据库的 DDL 在 staging 先验证

验证命令

bash
./mvnw -pl spring-open-starters/spring-open-starter-database -am test -DskipITs
./mvnw -pl spring-open-application -am test -DskipITs -DskipFrontend
git diff --check

涉及真实 MySQL / PostgreSQL 链路时按模块测试说明开启 Testcontainers。

相关

Released under the Apache License 2.0.