高级特性
本章将介绍 MyBatis Plus 的高级特性,包括枚举处理、类型处理器、SQL 注入器等功能。这些特性可以帮助你处理更复杂的业务场景。
枚举处理
在数据库设计中,我们经常使用整数或字符串来表示状态、类型等固定值。例如用户状态用 0/1 表示禁用/启用,订单状态用 10/20/30 表示已创建/已支付/已发货。
在 Java 代码中,直接使用数字会带来"魔法数字"问题:代码中充斥着难以理解的数字,可读性差,维护困难。使用枚举可以让代码更清晰、类型更安全,但需要处理数据库值与枚举对象之间的转换。MyBatis Plus 提供了优雅的解决方案。
为什么需要枚举处理?
假设有一个用户状态字段,数据库存储 0(禁用)和 1(启用)。如果不使用枚举,代码可能这样写:
// 不推荐:使用魔法数字
if (user.getStatus() == 1) {
// 启用状态 —— 1 是什么意思?需要查看文档或数据库
}
// 稍好:使用常量
public class UserStatus {
public static final int DISABLED = 0;
public static final int ENABLED = 1;
}
if (user.getStatus() == UserStatus.ENABLED) {
// 启用状态 —— 代码更清晰了
}
// 推荐:使用枚举
if (user.getStatus() == UserStatus.ENABLED) {
// 启用状态 —— IDE 支持自动补全,类型安全
}
使用枚举的优势:
- 类型安全:编译期检查,避免传入错误值
- 代码可读:枚举名称即文档
- IDE 支持:自动补全、跳转定义
- 易于扩展:可以添加方法、属性、实现接口
方式一:使用 @EnumValue 注解
@EnumValue 注解标记枚举中哪个属性对应数据库存储的值:
import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum UserStatus {
DISABLED(0, "禁用"),
ENABLED(1, "启用");
@EnumValue // 标记这个字段的值会存储到数据库
private final Integer code;
private final String desc;
}
实体类中使用:
@Data
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
// 直接使用枚举类型
private UserStatus status;
}
数据库操作时自动转换:
// 插入时:枚举自动转为 code 值存储
User user = new User();
user.setName("张三");
user.setStatus(UserStatus.ENABLED);
userMapper.insert(user);
// 实际执行的 SQL: INSERT INTO t_user (name, status) VALUES ('张三', 1)
// 查询时:数据库值自动转为枚举对象
User dbUser = userMapper.selectById(1L);
System.out.println(dbUser.getStatus()); // 输出: ENABLED
System.out.println(dbUser.getStatus().getDesc()); // 输出: 启用
方式二:实现 IEnum 接口
实现 IEnum 接口可以更灵活地控制枚举值:
import com.baomidou.mybatisplus.annotation.IEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum OrderStatus implements IEnum<Integer> {
CREATED(10, "已创建"),
PAID(20, "已支付"),
SHIPPED(30, "已发货"),
COMPLETED(40, "已完成"),
CANCELLED(50, "已取消");
private final Integer code;
private final String desc;
@Override
public Integer getValue() {
return this.code;
}
}
实体类中使用:
@Data
@TableName("t_order")
public class Order {
@TableId(type = IdType.AUTO)
private Long id;
private String orderNo;
private OrderStatus status;
}
枚举条件查询
在条件构造器中使用枚举进行查询:
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
// 查询启用状态的用户
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, UserStatus.ENABLED);
List<User> users = userMapper.selectList(wrapper);
// 查询已支付或已发货的订单
LambdaQueryWrapper<Order> orderWrapper = new LambdaQueryWrapper<>();
orderWrapper.in(Order::getStatus, Arrays.asList(OrderStatus.PAID, OrderStatus.SHIPPED));
List<Order> orders = orderMapper.selectList(orderWrapper);
枚举序列化
默认情况下,Jackson 序列化枚举时会输出枚举名称(如 "ENABLED")。如果希望输出枚举值,可以使用 @JsonValue 注解:
import com.fasterxml.jackson.annotation.JsonValue;
@Getter
@AllArgsConstructor
public enum UserStatus {
DISABLED(0, "禁用"),
ENABLED(1, "启用");
@EnumValue
@JsonValue // JSON 序列化时输出这个值
private final Integer code;
private final String desc;
}
// 序列化结果
// {"status": 1} 而不是 {"status": "ENABLED"}
如果希望输出完整信息,可以重写 toString 方法:
@Getter
@AllArgsConstructor
public enum UserStatus {
DISABLED(0, "禁用"),
ENABLED(1, "启用");
@EnumValue
private final Integer code;
private final String desc;
@Override
public String toString() {
return this.desc;
}
}
类型处理器
类型处理器(TypeHandler)是 MyBatis 的核心组件,负责 Java 类型与 JDBC 类型之间的转换。当 MyBatis 执行 SQL 时,需要将 Java 对象设置到 PreparedStatement 中(参数设置),也需要将 ResultSet 中的数据映射为 Java 对象(结果映射)。TypeHandler 就是完成这两项工作的桥梁。
类型处理器的作用
假设数据库有一个字段存储 JSON 字符串,而 Java 中我们希望直接映射为对象:
// 数据库字段(VARCHAR)
// {"phone":"13800138000","address":"北京市朝阳区"}
// Java 对象
public class UserInfo {
private String phone;
private String address;
}
// 如果没有类型处理器,需要手动转换:
String json = resultSet.getString("user_info");
UserInfo userInfo = objectMapper.readValue(json, UserInfo.class);
// 有了类型处理器后:
UserInfo userInfo = user.getUserInfo(); // 自动转换
内置类型处理器
MyBatis Plus 扩展了类型处理器的功能,提供了以下内置处理器:
| 处理器 | 说明 | 使用场景 |
|---|---|---|
JacksonTypeHandler | 基于 Jackson 的 JSON 处理 | JSON 字段存储对象 |
FastjsonTypeHandler | 基于 Fastjson 的 JSON 处理 | JSON 字段存储对象 |
GsonTypeHandler | 基于 Gson 的 JSON 处理 | JSON 字段存储对象 |
选择哪个处理器取决于项目中使用的 JSON 库。Jackson 是 Spring Boot 默认的 JSON 库,推荐使用 JacksonTypeHandler。
JSON 字段处理
当数据库字段存储 JSON 字符串,而 Java 中需要映射为对象时,可以使用 JSON 类型处理器。这在存储配置信息、扩展属性等场景非常常见。
使用场景:
- 用户标签:存储多个标签,如
["Java", "Python", "MySQL"] - 用户配置:存储复杂的配置对象
- 商品属性:存储动态属性,不同商品类型有不同的属性
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import java.util.List;
@Data
@TableName(value = "t_user", autoResultMap = true) // 必须开启 autoResultMap!
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
/**
* JSON 字段自动转换为 List
* 数据库存储:["Java", "Python", "MySQL"]
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> tags;
/**
* JSON 字段自动转换为复杂对象
* 数据库存储:{"phone":"13800138000","address":"北京市朝阳区","age":25}
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo userInfo;
}
@Data
public class UserInfo {
private String phone;
private String address;
private Integer age;
}
重要提示: autoResultMap = true 是必须的。这是因为 MyBatis Plus 需要生成一个自定义的 ResultMap 来指定字段的类型处理器。如果没有这个配置,查询结果映射时不会使用类型处理器。
使用示例:
// 插入:对象自动转为 JSON 字符串
User user = new User();
user.setName("张三");
user.setTags(Arrays.asList("Java", "Python", "MySQL"));
UserInfo info = new UserInfo();
info.setPhone("13800138000");
info.setAddress("北京市朝阳区");
info.setAge(25);
user.setUserInfo(info);
userMapper.insert(user);
// 实际执行的 SQL:
// INSERT INTO t_user (name, tags, user_info)
// VALUES ('张三', '["Java","Python","MySQL"]', '{"phone":"13800138000","address":"北京市朝阳区","age":25}')
// 查询:JSON 字符串自动转为对象
User dbUser = userMapper.selectById(1L);
List<String> tags = dbUser.getTags(); // [Java, Python, MySQL]
UserInfo info = dbUser.getUserInfo(); // UserInfo 对象
注意事项:
-
空值处理:如果 JSON 字段为空或无效 JSON,解析会失败。建议在类型处理器中添加容错逻辑。
-
字段变更:修改 UserInfo 类的属性后,数据库中的历史 JSON 数据可能不兼容,需要考虑数据迁移。
-
查询性能:JSON 字段无法建立索引(MySQL 5.7+ 支持生成列索引),大数据量查询时需要注意性能。
自定义类型处理器
当内置处理器不能满足需求时,可以创建自定义类型处理器。
示例:密码加密处理器
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 密码加密类型处理器
* 存储时自动加密,读取时原样返回
*/
@MappedTypes(String.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class PasswordTypeHandler extends BaseTypeHandler<String> {
// 加密算法(示例用,实际应使用更安全的算法)
private String encrypt(String password) {
// 使用 BCrypt 或其他安全算法
return "encrypted:" + password;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
// 存储时加密
ps.setString(i, encrypt(parameter));
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
// 读取时返回原值(验证时需要重新加密比较)
return rs.getString(columnName);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getString(columnIndex);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return cs.getString(columnIndex);
}
}
在实体类中使用:
@Data
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
@TableField(typeHandler = PasswordTypeHandler.class)
private String password;
}
示例:PostgreSQL JSONB 处理器
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.postgresql.util.PGobject;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@MappedTypes({Object.class})
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonbTypeHandler<T> extends JacksonTypeHandler<T> {
private final Class<T> clazz;
public JsonbTypeHandler(Class<T> clazz) {
super(clazz);
this.clazz = clazz;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)
throws SQLException {
PGobject pgObject = new PGobject();
pgObject.setType("jsonb");
pgObject.setValue(toJson(parameter)); // toJson 来自父类
ps.setObject(i, pgObject);
}
}
注册类型处理器
自定义类型处理器需要注册到 MyBatis 容器中:
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
@Configuration
public class TypeHandlerConfig {
@Autowired
private SqlSessionFactory sqlSessionFactory;
@PostConstruct
public void registerTypeHandlers() {
TypeHandlerRegistry registry = sqlSessionFactory.getConfiguration().getTypeHandlerRegistry();
registry.register(PasswordTypeHandler.class);
registry.register(JsonbTypeHandler.class);
}
}
SQL 注入器
SQL 注入器允许你扩展 BaseMapper,添加自定义的通用方法。
自定义通用方法
假设我们需要一个"批量插入或更新"的方法,可以通过 SQL 注入器实现:
import com.baomidou.mybatisplus.core.inject.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
/**
* 批量插入或更新方法
*/
public class InsertOrUpdateBatchMethod extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass,
TableInfo tableInfo) {
String sql = "<script>\n" +
"INSERT INTO " + tableInfo.getTableName() + "\n" +
"<trim prefix=\"(\" suffix=\")\" suffixOverrides=\",\">\n" +
"<if test=\"list[0].id != null\">id,</if>\n" +
this.sqlSelectColumns(tableInfo, false) +
"</trim>\n" +
"VALUES\n" +
"<foreach collection=\"list\" item=\"item\" separator=\",\">\n" +
"<trim prefix=\"(\" suffix=\")\" suffixOverrides=\",\">\n" +
"<if test=\"item.id != null\">#{item.id},</if>\n" +
this.sqlSelectValues(tableInfo, false) +
"</trim>\n" +
"</foreach>\n" +
"ON DUPLICATE KEY UPDATE\n" +
this.sqlUpdateSet(tableInfo) +
"</script>";
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, "insertOrUpdateBatch",
sqlSource, null, null);
}
private String sqlSelectColumns(TableInfo tableInfo, boolean excludeId) {
StringBuilder sb = new StringBuilder();
tableInfo.getFieldList().forEach(field -> {
sb.append(field.getColumn()).append(",");
});
return sb.toString();
}
private String sqlSelectValues(TableInfo tableInfo, boolean excludeId) {
StringBuilder sb = new StringBuilder();
tableInfo.getFieldList().forEach(field -> {
sb.append("#{item.").append(field.getProperty()).append("},");
});
return sb.toString();
}
private String sqlUpdateSet(TableInfo tableInfo) {
StringBuilder sb = new StringBuilder();
tableInfo.getFieldList().forEach(field -> {
sb.append(field.getColumn())
.append(" = VALUES(")
.append(field.getColumn())
.append("),");
});
// 移除最后的逗号
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
}
return sb.toString();
}
}
注册 SQL 注入器
import com.baomidou.mybatisplus.core.inject.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.inject.ISqlInjector;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class MybatisPlusConfig {
@Bean
public ISqlInjector sqlInjector() {
return new DefaultSqlInjector() {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableHelper) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableHelper);
// 添加自定义方法
methodList.add(new InsertOrUpdateBatchMethod());
return methodList;
}
};
}
}
使用自定义方法
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 继承了 insertOrUpdateBatch 方法
}
// 使用
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void batchSave(List<User> users) {
userMapper.insertOrUpdateBatch(users);
}
}
全局配置
配置枚举包扫描
mybatis-plus:
# 枚举类扫描包
type-enums-package: com.example.enums
configuration:
# 默认枚举类型处理器
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
配置类型处理器包扫描
mybatis-plus:
# 类型处理器扫描包
type-handlers-package: com.example.handler
小结
本章我们学习了:
- 枚举处理:使用
@EnumValue注解或IEnum接口实现枚举与数据库值的自动转换 - 类型处理器:使用内置的 JSON 处理器,以及自定义类型处理器
- SQL 注入器:扩展 BaseMapper 添加自定义通用方法
- 全局配置:配置枚举包和类型处理器包扫描
这些高级特性让 MyBatis Plus 能够处理更复杂的业务场景,提高开发效率。