问题场景
Springboot使用MybatisPlus框架,数据库中SQL能查到值,但是代码中查不到,出现All elements are null等问题。以下MybatisPlus简称MP
表结构
CREATE TABLE `camel` (
`pwd` varchar(255),
`age` int(11)
);
INSERT INTO `camel` VALUES ('100', 0);
INSERT INTO `camel` VALUES ('200', 1);
INSERT INTO `camel` VALUES ('300', 2);
INSERT INTO `camel` VALUES ('400', NULL);
Java实体类
@TableName(value = "camel")
@Data
public class Camel {
@TableField("pwd")
private String pass_word;
@TableField("age")
private Integer age;
}
SpringBoot测试类
@SpringBootTest
class DemoApplicationTests {
@Autowired
CamelMapper camelMapper;
@Test
void testTableField() {
// 构造查询条件 pwd > 100 的 LambdaQueryWrapper
LambdaQueryWrapper<Camel> lambdaQueryWrapper = Wrappers.lambdaQuery(Camel.class)
.gt(Camel::getPass_word, "100");
List<Camel> list = camelMapper.selectList(lambdaQueryWrapper);
list.forEach(System.out::println);
assert list.get(0).getPass_word()!=null;
}
}
执行结果
代码执行结果与预期不符,数据库中能查询到pwd,代码中查到的pass_word为NULL
先说原因
pass_word字段不满足命名规范,导致MP框架无法通过反射将查询字段赋值给实体类属性。
思考
@TableField注解的value属性,常是用于解决数据库字段名与实体类的属性名不一致时的问题,先前认为使用该注解就能结果上述问题。查看源码中MP给出的注释,如下
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface TableField {
/**
* 数据库字段值
* <p>
* 不需要配置该值的情况:
* <li> 当 {@link com.baomidou.mybatisplus.core.MybatisConfiguration#mapUnderscoreToCamelCase} 为 true 时,
* (mp下默认是true,mybatis默认是false), 数据库字段值.replace("_","").toUpperCase() == 实体属性名.toUpperCase() </li>
* <li> 当 {@link com.baomidou.mybatisplus.core.MybatisConfiguration#mapUnderscoreToCamelCase} 为 false 时,
* 数据库字段值.toUpperCase() == 实体属性名.toUpperCase() </li>
*/
String value() default "";
当使用@TableField注解,执行sql如下:
select 注解值 as 实体类属性名 from table;
@TableField注解是通过别名实现的,其作用是当resultType为实体类时,自动为查询结果设置别名,是发生在查询阶段,也因此将控制台打印的sql拿到数据库中能正常执行,而本次问题是发生在用实体类接收查询结果阶段。
源码分析
容器启动后,MP会缓存Mapper实体类的setter与getter方法集合,这里使用了Lombok自动生成setter与getter
开始执行查询方法后,首先预编译PrepareStatement
当配置了Mybatis或MP的日志实现,控制台会打印预编译sql与参数,就是在这里实现的
预编译参数赋值,这里就是查询条件中的100
查询SQL执行完成,处理查询结果的映射关系
根据是否打开驼峰自动转换开关,处理属性名,注意这个开关在Mybatis中是默认关闭,而在MP中是默认打开的!所以在这里的pass_word被替换成了password
之后从容器启动时MP缓存的实体类的setter与getter方法集合中,查找实体类属性的setter方法,这里可以找到age,但是pass_word因被替换成password,导致无法找到对应的setter方法
接下用实体类接收字段查询结果不为NULL,且符合命名规范的属性
通过反射执行上面找到的setter方法
可以看到只有符合命名规范的字段才会被赋值成功
最终返回集合中只会有符合命名规范的字段,这里size=3,但集合中只有两个元素,是因为SQL查出了3条记录,其中pwd=400的记录,因password字段赋值失败为NULL,且对应的age也为NULL,该对象的所有属性都为NULL,所以集合中就存了一个NULL对象,如果集合中所有元素都是NULL,就会size != 0,但是All elements are null
总结
MP通过反射的方式,使用属性对应setter方法为属性赋值,将查询结果映射到实体类,当属性命名不规范,且开启了驼峰命名开关,就会无法找到对应setter方法,导致属性无法赋值成功。
解决办法
方法一:手动添加setter方法
private void setPassword(String pass_word) {
this.pass_word = pass_word;
}
方法二:若使用Lombok,规范属性命名(可以保留命名不规范的属性,避免影响项目原功能),本质与方法一相同
private String password;
方法三:在配置文件中关闭驼峰自动映射
mybatis-plus.configuration.map-underscore-to-camel-case=false
方法四:自定义Mapper方法,不使用实体类作为resultType,在xml中使用自定义resultMap,指定查询结果字段与实体类属性对应关系(没有使用resultType,@TableField注解不会生效)
<!--property="pass_word"是实体类属性名,最终执行的setter方法是setPass_word-->
<!--column="pwd"是查询结果字段名-->
<resultMap id="myResultMap" type="com.example.demo.generator.domain.Camel">
<result property="pass_word" column="pwd"/>
<result property="age" column="age"/>
</resultMap>
<select id="myList" resultMap="myResultMap">
select pwd, age from camel
</select>