开发易忽视问题:mybatis-plus @TableName注解如何工作

2,235 阅读7分钟

MyBatis-Plus 是 MyBatis 的增强工具,它简化了数据库 CRUD 操作以及其他常见操作。其中,@TableName 注解用于指定实体类与数据库表的映射关系。具体来说,@TableName 注解可以用来明确指示某个实体类对应的数据库表名。

@TableName 注解

首先,让我们看看 @TableName 注解的定义:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TableName {
    String value() default "";
}
  • value: 指定数据库表的名称。如果不指定,则默认使用实体类的名称。

工作机制

@TableName 注解的工作机制与 MyBatis-Plus 中的元数据处理有关。当应用程序启动时,MyBatis-Plus 会进行初始化扫描,解析所有包含该注解的实体类,并将这些映射信息存储到内部元数据结构中。在执行 SQL 操作时,MyBatis-Plus 会根据这些元数据生成相应的 SQL 语句。

以下是 @TableName 注解如何与数据库建立映射的详细步骤:

1. 扫描和解析注解

在应用程序启动时,MyBatis-Plus 会扫描所有实体类,并解析这些类上的注解。对于每一个包含 @TableName 注解的实体类,MyBatis-Plus 会读取其注解值,以确定该实体类对应的数据库表名。

@TableName("user")
public class User {
    private Long id;
    private String name;
    private Integer age;

    // getters and setters
}

在上述例子中,User 类被注解为对应数据库中的 user 表。

2. 存储元数据信息

MyBatis-Plus 会将解析得到的表名信息存储在内部数据结构中,例如 TableInfo 对象。TableInfo 是 MyBatis-Plus 用于管理实体类与表映射关系的核心类之一。

public class TableInfo {
    private String tableName; // 数据库表名
    private String entityType; // 实体类类型
    // 其他表相关的元数据属性
}

当解析到 User 类时,会创建一个 TableInfo 对象,并设置其 tableName 属性为 "user"

3. 生成 SQL 语句

在实际执行数据库操作时,MyBatis-Plus 会基于存储的元数据信息,自动生成相应的 SQL 语句。例如,当调用 selectById 方法查询用户时,MyBatis-Plus 会使用 TableInfo 中的表名信息生成如下 SQL 语句:

SELECT * FROM user WHERE id = ?

4. 映射结果集

当查询结果返回后,MyBatis-Plus 会将结果集映射回相应的实体类对象,这个过程也是基于元数据结构完成的。它会使用反射机制将数据库行数据填充到实体类的属性中。

User user = userMapper.selectById(1);

以上代码最终会查询 user 表中的记录,并将结果映射到 User 对象。

底层实现细节

MyBatis-Plus 在底层通过拦截器、元数据处理器等机制实现了注解解析和元数据存储。以下是一些关键类和方法:

  • AnnotationProcessor: 用于扫描和解析注解。
  • TableInfoHelper: 管理和存储 TableInfo 对象的辅助类。
  • SqlInjector: 负责生成 SQL 语句的类,通过注入方式扩展 MyBatis 的功能。

1. 扫描和解析注解

在应用程序启动时,MyBatis-Plus 会扫描所有实体类并解析其上的注解。这一过程通常会涉及 Spring 框架的组件扫描机制。

1.1 配置 MyBatis-Plus 与 Spring Boot 集成

首先,需要在 Spring Boot 应用中配置 MyBatis-Plus。例如,在 application.yml 文件中进行配置:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/yourdb
    username: yourusername
    password: yourpassword
  mybatis-plus:
    mapper-locations: classpath:/mapper/**/*.xml
    type-aliases-package: com.example.entity

这里的 type-aliases-package 指定了包含实体类的包路径,MyBatis-Plus 会在此包路径下扫描实体类。

1.2. 自动配置类 MybatisPlusAutoConfiguration

Spring Boot 提供了一种自动配置机制,通过 @EnableAutoConfiguration 注解触发。在 MyBatis-Plus 中,有一个自动配置类 MybatisPlusAutoConfiguration

@Configuration
@ConditionalOnClass(SqlSessionFactory.class)
@EnableConfigurationProperties(MybatisPlusProperties.class)
public class MybatisPlusAutoConfiguration {
    
    // DataSource, SqlSessionFactory, SqlSessionTemplate 等的定义和配置
    
}
1.3. 扫描实体类
1.3.1 使用 TypeAliasesPackageScanHandler

MyBatis-Plus 在初始化时,会使用 TypeAliasesPackageScanHandler 来扫描指定包路径下的实体类。该类会根据 mybatis-plus.type-aliases-package 配置扫描包路径内的所有类,并将这些类注册为别名。

public class TypeAliasesPackageScanHandler {
    
    public void registerTypeAliases(Configuration configuration, String typeAliasesPackage) {
        if (typeAliasesPackage != null && !typeAliasesPackage.isEmpty()) {
            String[] typeAliasPackages = typeAliasesPackage.split(",");
            for (String packageToScan : typeAliasPackages) {
                Set<Class<?>> classSet = scanClasses(packageToScan);
                for (Class<?> clazz : classSet) {
                    configuration.getTypeAliasRegistry().registerAlias(clazz);
                }
            }
        }
    }
    
    private Set<Class<?>> scanClasses(String basePackage) {
        // 使用 Spring 的 ClassPathScanningCandidateComponentProvider 扫描指定包路径下的类
        Set<Class<?>> classes = new HashSet<>();
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
        scanner.addIncludeFilter(new AssignableTypeFilter(Object.class));
        
        for (BeanDefinition bd : scanner.findCandidateComponents(basePackage)) {
            try {
                classes.add(Class.forName(bd.getBeanClassName()));
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return classes;
    }
}
1.3.2 解析注解

一旦找到实体类,MyBatis-Plus 会利用反射机制解析这些类上的注解,例如 @TableName, @TableId, @TableField 等。MyBatis-Plus 在运行时通过这些注解来确定实体类与数据库表和字段之间的映射关系。

例如,对于一个简单的实体类:

@TableName("user")
public class User {
    
    @TableId(type = IdType.AUTO)
    private Long id;
    
    @TableField("username")
    private String userName;
    
    // other fields and methods...
}

MyBatis-Plus 会在加载该类时读取 @TableName 注解以了解对应的表名,读取 @TableId@TableField 注解以了解主键和字段的映射信息。

2. 存储元数据信息

MyBatis-Plus 使用 TableInfoHelper 类来管理和存储 TableInfo 对象,TableInfo 对象包含了实体类与表之间的映射信息。

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;

public class TableInfoHelper {

    public static TableInfo initTableInfo(Class<?> clazz) {
        // 检查类上是否有 @TableName 注解
        TableName table = clazz.getAnnotation(TableName.class);
        String tableName = (table != null && !table.value().isEmpty()) ? table.value() : clazz.getSimpleName();

        TableInfo tableInfo = new TableInfo();
        tableInfo.setTableName(tableName);
        tableInfo.setEntityType(clazz);

        // 存储到缓存中
        TableInfoCache.put(clazz, tableInfo);
        return tableInfo;
    }
}

initTableInfo 方法会读取实体类上的 @TableName 注解,并将解析得到的表名和其他元数据信息存储到 TableInfo 对象中,然后将其缓存起来,以便后续使用。

3. 生成 SQL 语句

MyBatis-Plus 使用 SqlInjector 类来动态生成 SQL 语句。SqlInjector 是 MyBatis-Plus 提供的扩展点,可以通过它来定制化 SQL 的生成逻辑。在执行具体操作时,例如 selectById,MyBatis-Plus 会从缓存中获取对应的 TableInfo 信息,并根据它生成相应的 SQL 语句。

public class DefaultSqlInjector implements ISqlInjector {

    @Override
    public void injectMappedStatement(MybatisConfiguration configuration, Class<?> mapperClass, TableInfo tableInfo) {
        // 示例:生成 selectById 方法的 SQL 语句
        addSelectByIdMappedStatement(mapperClass, tableInfo);
    }

    private void addSelectByIdMappedStatement(Class<?> mapperClass, TableInfo tableInfo) {
        String sql = "SELECT * FROM " + tableInfo.getTableName() + " WHERE id = #{id}";
        MappedStatement mappedStatement = new MappedStatement.Builder(configuration, "selectById", sql)
                .resultMaps(resultMaps)
                .build();
        configuration.addMappedStatement(mappedStatement);
    }
}

上述代码展示了 DefaultSqlInjector 如何为 selectById 方法生成 SQL 语句,它会根据 TableInfo 中存储的表名信息来构建 SQL 语句。

4. 映射结果集

查询结果返回后,MyBatis-Plus 会使用 ResultMap 来将结果集映射回相应的实体类对象。这个过程通常涉及反射机制,将查询结果逐字段赋值给实体类的属性。

// 示例:定义一个 ResultMap
ResultMap resultMap = new ResultMap.Builder(configuration, "UserResultMap", User.class, resultMappings).build();
configuration.addResultMap(resultMap);

在实际执行查询时,例如 userMapper.selectById(1L),MyBatis-Plus 会使用预先定义好的 ResultMap 将查询结果映射回 User 对象。

5. 实际执行查询

当调用 userMapper.selectById(1L) 时,MyBatis-Plus 会使用预先定义好的 MappedStatementResultMap 执行查询并映射结果。

查询方法

我们首先需要在 Mapper 接口中定义查询方法,例如:

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface UserMapper extends BaseMapper<User> {
}

BaseMapper 接口已经包含了常见的 CRUD 操作方法,包括 selectById。所以,我们可以直接使用这些方法进行查询。

执行查询

当调用 userMapper.selectById(1L) 时,MyBatis-Plus 会生成对应的 SQL 语句,并通过 MyBatis 的执行器执行该语句。执行结果会被映射到相应的实体类对象中。

// Service 层调用
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
}

// Controller 层调用
@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.getUserById(id);
    }
}

6. 映射机制

MyBatis-Plus 使用 MyBatis 提供的 ResultMap 机制将查询结果集映射为实体类对象。以下是一个简化的示例,展示了这一过程:

public class UserTypeHandler implements TypeHandler<User> {

    @Override
    public void setParameter(PreparedStatement ps, int i, User parameter, JdbcType jdbcType) throws SQLException {
        // 设置参数
    }

    @Override
    public User getResult(ResultSet rs, String columnName) throws SQLException {
        User user = new User();
        user.setId(rs.getLong("id"));
        user.setName(rs.getString("name"));
        user.setAge(rs.getInt("age"));
        return user;
    }

    @Override
    public User getResult(ResultSet rs, int columnIndex) throws SQLException {
        return getResult(rs, rs.getMetaData().getColumnName(columnIndex));
    }

    @Override
    public User getResult(CallableStatement cs, int columnIndex) throws SQLException {
        return getResult(cs, cs.getMetaData().getColumnName(columnIndex));
    }
}

在实际应用中,MyBatis-Plus 自动处理了上述过程,你只需定义好实体类和 Mapper 接口,即可完成复杂的数据库操作。

完整流程回顾

  1. 类扫描:Spring Boot 在启动时扫描指定包中的所有类。
  2. 注解解析:MyBatis-Plus 通过 TableInfoHelper 类解析实体类上的注解,并存储元数据信息到 TableInfo 对象中。
  3. SQL 生成SqlInjector 根据存储在 TableInfo 中的信息生成相应的 SQL 语句。
  4. 执行查询:当调用查询方法时,MyBatis-Plus 使用预定义的 MappedStatement 执行 SQL 查询。
  5. 结果映射:查询结果通过 ResultMap 映射回对应的实体类对象。