MyBatis 数据库
这篇解决什么
给新表写 DO 和 Mapper,单表分页用 selectPage,联表分页用 selectJoinPage。读完能改 AdminUserDO / AdminUserMapper 这一路,并知道逻辑删除、自动填充、JSON / 加密字段怎么挂。
DO 转 VO、字段翻译见 VO 对象转换与数据翻译。多租户字段见 多租户。模块怎么拆见 项目结构。
默认端口 48080,管理端前缀 /admin-api。
组件位置
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| Starter | MyBatis Plus、分页插件、自动填充、Join | ruoyi-office/yudao-framework/yudao-spring-boot-starter-mybatis/ |
BaseDO | 审计字段 + @TableLogic | .../mybatis/core/dataobject/BaseDO.java |
TenantBaseDO | BaseDO 加 tenantId | yudao-spring-boot-starter-biz-tenant/.../TenantBaseDO.java |
BaseMapperX | 单表 / 联表 CRUD 与分页 | .../mybatis/core/mapper/BaseMapperX.java |
LambdaQueryWrapperX | 空值不进 SQL | .../mybatis/core/query/LambdaQueryWrapperX.java |
MPJLambdaWrapperX | 联表条件,同样有 xxxIfPresent | .../mybatis/core/query/MPJLambdaWrapperX.java |
| 配置 | id-type、逻辑删除值、加密密钥 | ruoyi-office/yudao-server/src/main/resources/application.yaml |
| 范文 | 用户 DO / Mapper | yudao-module-system 的 AdminUserDO、AdminUserMapper |
YudaoMybatisAutoConfiguration 会扫描带 @Mapper 的接口,并注册 DefaultDBFieldHandler、PaginationInnerInterceptor。
请求怎么落到表
列表接口不在 Controller 里拼 Wrapper。Controller 收 VO,Service 编排,Mapper 查表。

示意图:管理端用户列表落到 AdminUserMapper,再进 system_users。
getUserPage 在 AdminUserServiceImpl:先按角色 / 部门收窄 userIds、deptIds,再调用 userMapper.selectPage。
实体
DO 放 dal.dataobject,类名以 DO 结尾。注解先对齐表,再写业务字段。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
@TableName | 表名。JSON / 加密字段必须 autoResultMap = true | AdminUserDO → system_users |
@TableId | 主键,推荐 Long 自增 | AdminUserDO.id |
@KeySequence | Oracle / PostgreSQL 等序列;MySQL 可不写 | AdminUserDO、DataSourceConfigDO |
@TableField(fill) | 插入 / 更新自动填 | BaseDO 的审计字段 |
@TableLogic | 逻辑删除 | BaseDO.deleted |
@TableField(typeHandler) | JSON、集合、加密 | AdminUserDO.postIds、DataSourceConfigDO.password |
@TableName(value = "system_users", autoResultMap = true)
@KeySequence("system_users_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class AdminUserDO extends TenantBaseDO {
@TableId
private Long id;
private String username;
@TableField(typeHandler = JacksonTypeHandler.class)
private Set<Long> postIds;
// ...
}postIds 在库里是 JSON 字符串。没有 autoResultMap = true,TypeHandler 读不回来。
BaseDO 字段
createTime / updateTime 是 LocalDateTime,不是 Date。creator / updater 是 String,存登录用户 id。
| 名称 | 说明 | 填充 |
|---|---|---|
createTime | 创建时间 | FieldFill.INSERT |
updateTime | 最后更新时间 | FieldFill.INSERT_UPDATE |
creator | 创建人 id | FieldFill.INSERT |
updater | 更新人 id | FieldFill.INSERT_UPDATE |
deleted | 逻辑删除,0 未删、1 已删 | @TableLogic |
DefaultDBFieldHandler 在插入、更新时补这些字段。登录用户来自 SecurityFrameworkUtils.getLoginUserId(),写入时转成字符串。
TenantBaseDO 只多 tenantId。AdminUserDO 继承它。拦截器怎么拼租户列,见 多租户。
application.yaml 里 id-type 默认 NONE:按数据源自动选 AUTO 或 INPUT。要改成雪花,把 mybatis-plus.global-config.db-config.id-type 设为 ASSIGN_ID,并去掉实体上的 @KeySequence。
不要用 DO 当接口出入参
创建用户只该收账号、昵称这些业务字段。用 DO 会让 createTime、creator 也能被传入。Swagger 和校验挂在 VO 上,DO 只映表。
整颗 DO 从前端回传再更新前,先 clean(),清掉 creator / createTime / updater / updateTime。
逻辑删除不要自己拼
BaseMapperX 的 SELECT / UPDATE / DELETE 会自动带 deleted = 0。不要关 @TableLogic 又手写 deleted = 0。要查已删行,只能手写 XML 或 @Select。
唯一索引带上 deleted
逻辑删除后再插入同一业务键,会撞唯一索引。建唯一键时把 deleted(以及需要时的 tenant_id)加进去,例如资产表的 uk_asset_code_tenant。
编码约定
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 包 | DO 在 dal.dataobject,Mapper 在 dal.mysql | 例如 .../dal/mysql/user/AdminUserMapper.java |
| 注释 | 外键、枚举、冗余写清楚 | AdminUserDO.sex、status |
| 查询放 Mapper | Service 不直接 new LambdaQueryWrapper | TenantMapper.selectPage |
| 方法名 | selectBy条件 | selectByUsername、selectListByDeptIds |
| 条件 | 优先 Lambda,少写列名字符串 | AdminUserDO::getUsername |
简单单表查询用 Mapper 的 default 方法,方便复用。
为什么查询不写在 Service
同样的 selectByUsername 会在登录、导入、校验里复用。散落在 Service 里会复制同一段 Wrapper,也难聚焦业务。
Mapper 方法
Mapper 继承 BaseMapperX<T>,它再继承 MyBatis Plus Join 的 MPJBaseMapper。入参分页用 PageParam,出参用 PageResult。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
selectOne | 按一到三个字段查一条 | AdminUserMapper.selectByUsername |
selectCount | 条件计数 | TenantMapper.selectCountByPackageId |
selectList | 多条;in 空集合直接返回空列表 | selectListByDeptIds |
selectPage | PageParam → IPage → PageResult | TenantMapper.selectPage、AdminUserMapper.selectPage |
selectJoinPage | 联表分页,结果可投影到别的 class | CrmBusinessMapper.selectPage、WmsItemSkuMapper.selectPage |
insertBatch | 批量插入;SQL Server 会改成逐条 | BaseMapperX.insertBatch |
pageSize 为 PageParam.PAGE_SIZE_NONE(-1)时改成查全部,导出接口常用。
@Mapper
public interface TenantMapper extends BaseMapperX<TenantDO> {
default PageResult<TenantDO> selectPage(TenantPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<TenantDO>()
.likeIfPresent(TenantDO::getName, reqVO.getName())
.likeIfPresent(TenantDO::getContactName, reqVO.getContactName())
.likeIfPresent(TenantDO::getContactMobile, reqVO.getContactMobile())
.eqIfPresent(TenantDO::getStatus, reqVO.getStatus())
.betweenIfPresent(TenantDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(TenantDO::getId));
}
}AdminUserMapper.selectByUsername 就是 selectOne(AdminUserDO::getUsername, username)。带部门、关键字的用户分页看同源文件的 selectPage。
并发下可能插出多条时,不要用会报「期望一条」的 selectOne,改用 selectFirstOne 或 selectLastOne。行锁用 selectOneForUpdate,必须包在事务里。
条件构造器
LambdaQueryWrapperX、QueryWrapperX 在 Plus 的 Wrapper 上加了 xxxIfPresent:值为空就不拼进 SQL。
| 名称 | 说明 |
|---|---|
likeIfPresent / likeRightIfPresent | 字符串有内容才 like |
eqIfPresent / neIfPresent | 相等 / 不等 |
inIfPresent | 集合或数组非空才 in |
gtIfPresent / geIfPresent / ltIfPresent / leIfPresent | 比较 |
betweenIfPresent | 两个都有走 between;只传一端则 >= 或 <= |
联表用 MPJLambdaWrapperX,方法同名,列可以用主表或副表的方法引用。
联表分页
单表能解决就多次查询、内存拼。必须 Join 时,先写 MPJLambdaWrapperX,不要先上 XML。
MPJLambdaWrapperX<WmsItemSkuDO> query = new MPJLambdaWrapperX<WmsItemSkuDO>()
.selectAll(WmsItemSkuDO.class)
.innerJoin(WmsItemDO.class, WmsItemDO::getId, WmsItemSkuDO::getItemId)
.likeIfPresent(WmsItemDO::getName, reqVO.getItemName())
.likeIfPresent(WmsItemSkuDO::getCode, reqVO.getCode())
.orderByDesc(WmsItemSkuDO::getId);
return selectJoinPage(reqVO, WmsItemSkuDO.class, query);完整实现在 yudao-module-wms 的 WmsItemSkuMapper。CRM 商机列表在 CrmBusinessMapper.selectPage 里用同一套 selectJoinPage 拼数据权限。
需要把副表字段平铺或内嵌时,把目标 class 传给 selectJoinPage / selectJoinList:selectAs 平铺一列,selectAssociation 嵌一整段对象。不要把多表列全堆回主 DO。
application.yaml 的 mybatis-plus-join.sub-table-logic 默认 true,副表也会加逻辑删除。
XML 分页有两种:自己写 LIMIT + COUNT;或方法入参 / 返回用 IPage,只写查询 SQL,由分页插件补条数。能用第二种就用第二种,总条数为 0 时不会再查列表。
XML 列表要自己核数据权限
部门数据权限走 SQL 改写。复杂手写 XML 可能拼不上规则。能用 Wrapper 的列表不要改成 XML。
复杂字段
TypeHandler 做 Java 类型和 JDBC 类型转换。自定义实现放 cn.iocoder.yudao.framework.mybatis.core.type。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
JacksonTypeHandler | JSON ↔ Java 对象 / 集合 | AdminUserDO.postIds |
EncryptTypeHandler | 字段 AES 加解密 | DataSourceConfigDO.password、OaMailAccountDO |
LongListTypeHandler 等 | 逗号分隔的 id 列表 | .../mybatis/core/type/ |
@TableName(value = "infra_data_source_config", autoResultMap = true)
public class DataSourceConfigDO extends BaseDO {
@TableField(typeHandler = EncryptTypeHandler.class)
private String password;
}密钥是 mybatis-plus.encryptor.password。加密列只能等值查询,条件里要先 EncryptTypeHandler.encrypt(...)。不能 like。
Mapper XML
XML 放各 yudao-module-xxx-server 的 src/main/resources/mapper/。统计、窗口函数这类 Wrapper 写不清的 SQL 再放这里,例如 VisitStatsMapper.xml。
动态 WHERE 仍优先 ifPresent 或 <if test>,少拼字符串。分页不要和 selectPage 两套计数混用。
配置与操作
Mapper 继承 BaseMapperX,条件用 LambdaQueryWrapperX。XML 只放 Wrapper 写不清的 SQL。改数据源 yaml 要重启。
开启见 框架层。
