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 元数据层的主线。

posted @ 2026-09-21 09:58  01o00o10  阅读(17)  评论(0)    收藏  举报