MyBatis-Plus 源码阅读(三):一个实体类是怎么变成 TableInfo
MyBatis-Plus 源码阅读(三):一个实体类是怎么变成 TableInfo
说明:本文基于 MyBatis-Plus 3.5.9,不同小版本的方法名和初始化顺序可能略有差异。
MyBatis-Plus 生成 SQL 之前,得先回答一些很朴素的问题:
- 表叫什么;
- 主键是哪一个;
- Java 属性和数据库列怎么对应;
- 哪些字段可以插入、更新;
- 有没有逻辑删除、乐观锁、自动填充。
这些答案最后都放在 TableInfo 里。
它不是从数据库里查出来的
先说一个容易想错的地方:TableInfo 默认不是 MyBatis-Plus 启动时查询数据库元数据得到的。
它主要来自实体类、注解和全局配置。
@TableName("mp_user")
public class User {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@TableField("user_name")
private String name;
}
MyBatis-Plus 会通过反射读这个类,再结合 GlobalConfig.DbConfig,整理出表名、主键和字段列表。
数据库里真实存在什么列,只有执行 SQL 时数据库才会校验。实体上写错列名,TableInfo 初始化通常不会替你发现。
TableInfo 在什么时候创建
在 MyBatis-Plus 启动、解析 Mapper 并注入基础 SQL 方法时,AbstractSqlInjector.inspectInject() 会解析 Mapper 的实体泛型:
UserMapper extends BaseMapper<User>
↓
User.class
接着调用:
TableInfoHelper.initTableInfo(
builderAssistant,
User.class
);
TableInfoHelper 内部有缓存。一个实体正常只需要解析一次,后面的 SQL 方法共享同一份元数据。
主流程可以缩成:
initTableInfo()
-> initTableName()
-> initTableFields()
-> 初始化/注册 ResultMap
-> 执行 PostInitTableInfoHandler 扩展
-> 放入缓存
注意:不同版本中,扩展处理和放入缓存的先后顺序可能略有差异,读源码时以当前版本为准。
表名怎么确定
initTableName() 先看 @TableName。
有明确配置时优先使用:
@TableName("mp_user")
没有注解时,默认从类名推导,例如 UserAccount 转成 user_account。
这个过程还会考虑:
- 全局表前缀
tablePrefix; - schema;
- 是否开启下划线命名
tableUnderline; - 是否开启大写命名
capitalMode; - 表名格式
tableFormat; @TableName(keepGlobalPrefix = true)是否保留全局前缀。
所以最终表名不是只由 @TableName 决定,而是“实体注解优先,全局配置参与推导或兜底”。如果 @TableName 明确指定了表名,全局表前缀默认不再拼接,除非 keepGlobalPrefix = true。
主键怎么找
initTableFields() 会先看字段中有没有 @TableId。
有注解时,使用被标记的字段,并处理注解里的列名和 IdType。没有注解时,它会尝试把名为 id 的属性当作主键。
列名和 IdType 仍可能受到 @TableField、全局 columnUnderline、全局 id-type 等配置影响。
如果一直没找到主键,TableInfo.havePK() 就是 false。后面 DefaultSqlInjector 不会注入 selectById、updateById、deleteById 这一类依赖主键的方法。
也就是说,实体没有主键并不是所有功能都不能用,而是按 ID 操作不会得到对应的语句。insert、selectList 等不依赖主键的方法仍然可能可用。
普通字段里藏了不少开关
每个普通字段会变成一个 TableFieldInfo。它不只保存属性名和列名,还保存:
- insert、update、where 字段策略;
- 是否参与 select;
- TypeHandler 和 JDBC 类型;
- 是否自动填充;
- 是否为逻辑删除字段;
- 是否为乐观锁字段;
- 默认排序信息。
例如:
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@Version
private Integer version;
@TableLogic
private Integer deleted;
这些注解不会立刻执行填充、加锁或删除。它们只是先把标记写进 TableFieldInfo,等 SQL 生成或参数处理时再使用。
需要补充的是:逻辑删除、乐观锁、自动填充真正生效,通常还需要额外配置。例如:
- 自动填充需要实现
MetaObjectHandler; - 乐观锁需要注册
OptimisticLockerInnerInterceptor; - 逻辑删除可以通过
@TableLogic或全局配置开启。
TableInfo 为什么这么重要
后面的很多功能不再重复反射实体,而是直接问 TableInfo:
Insert -> 哪些字段可插入
UpdateById -> 主键列、可更新字段、版本字段
DeleteById -> 是否逻辑删除
SelectList -> 查询列、逻辑删除条件、默认排序
参数处理器 -> 主键类型、填充字段
乐观锁插件 -> version 对应哪个字段
这就是元数据层存在的意义:实体只解析一次,后面的模块都用同一种描述。
继承字段怎么处理
TableInfoHelper 不只看当前类声明的字段。它会收集父类中的字段,因此公共基类可以正常工作:
public class BaseEntity {
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
public class User extends BaseEntity {
private Long id;
private String name;
}
但字段隐藏、重复 @TableId、泛型层级过深时,反射结果会更难判断。读这部分源码时,要同时留意 ReflectionKit 和 MyBatis 的 Reflector。
动手看一眼缓存
给 User 增加几个字段:
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@Version
private Integer version;
@TableLogic
private Integer deleted;
确保 Mapper 已经被 MyBatis-Plus 初始化,例如 Spring 容器启动完成后,再执行:
TableInfo tableInfo =
TableInfoHelper.getTableInfo(User.class);
assertNotNull(tableInfo);
assertEquals("mp_user", tableInfo.getTableName());
assertEquals("id", tableInfo.getKeyProperty());
assertTrue(tableInfo.isWithVersion());
assertTrue(tableInfo.isWithLogicDelete());
assertTrue(tableInfo.isWithInsertFill());
这里的断言验证的是 TableInfo 中的元数据标记,不代表填充、逻辑删除、乐观锁一定已经在业务中生效。
TableInfo 可以理解为 MyBatis-Plus 的“实体元数据中心”。它把实体类、注解、全局配置整理成统一描述,后续 SQL 注入、参数处理、逻辑删除、乐观锁、自动填充等模块都围绕它工作。理解 TableInfo 的初始化过程,基本就理解了 MyBatis-Plus 元数据层的主线。
本文来自博客园,作者:01o00o10,转载请注明原文链接:https://www.cnblogs.com/01o00o10/articles/23050336

浙公网安备 33010602011771号