解析result元素
在了解了resultMap元素的属性定义之后,我们来细看resultMap元素的子元素定义:
<!ELEMENT resultMap (constructor?,id*,result*,association*,collection*, discriminator?)>
普通的result元素
resultMap有六个子元素定义,最常见也是最普通的莫过于result元素了:
<!ELEMENT result EMPTY>
<!ATTLIST result
property CDATA #IMPLIED
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
>
result元素是结果映射的基础,用于将数据库查询结果中某一列的值映射为一个java的简单数据类型(String, int, double, Date 等)。
通常来说,resultMap的每个result子元素都与resultMap对应的PO对象的某个属性相对应。
property属性
mybatis会根据property属性的取值关联result元素和PO对象属性的关系,因此,通常来说property属性的取值应该指向PO对象的某个具体的属性名称。

但是实际上,property属性的取值将会被转换为属性描述符(PropertyTokenizer)对象来进行处理,因此property属性的取值是可以使用常见的点式分隔形式进行复杂属性导航的。
有关于
PropertyTokenizer相关的内容,参见前面的文章。
我们以一个简单的示例来看一下:
首先我们定义两个简单的PO:
class People{
private String name;
}
class Company{
private People boss;
}
配置一个resultMap,他的result的property属性使用了点式分隔形式,表示将查询到的名称为name的数据列的值赋值给**.Company对象的boss属性的name属性:
<resultMap id="company" type="**.Company">
<result property="boss.name" column="name" />
</resultMap>
假设我们查询到的数据结果是:
| name |
|---|
| JPanda |
那么下面就是我们最终得到的Company对象的JSON表现形式:
{
"boss":{
"name":"JPanda"
}
}
详细的代码示例参见单元测试:gitee.com/topanda/myb…
虽然在dtd定义中,property是一个选填的属性,但是如果我们在resultMap元素下直接使用result子元素,那么property属性就是必填的,如果不配置property属性,那么当前result子元素指定的数据列将不会被映射到实体对象中,也就是说column属性对应的数据会丢失。
那么,为什么property属性会被设计成一个可选的属性呢?
这是因为,result元素在设计上不仅可以作为resultMap的子元素出现,还可以作为collection,association以及discriminator的case的子元素出现,result元素出现位置的不同,其所对应PO对象属性名称的取值方式也稍有不同,在部分场景下property属性并不是获取PO对象属性名称取值的唯一方式,因此property属性自然而然也就不是必填选项了,这一点在文章的后面,我们会逐步了解到。
collection,association以及discriminator元素同属于resultMap的子元素。
javaType属性
result元素的javaType属性表示PO对象的属性类型,这是一个可选的属性,因为在获取属性名称和PO对象类型的前提下,我们可以很方便的通过反射获取到属性对应的javaType。
一个简单的通过反射获取属性类的的示例代码:
class PO {
private String id;
}
@Test
@SneakyThrows
public void loadJavaType() {
String property = "id";
Field field = PO.class.getDeclaredField(property);
assert String.class.isAssignableFrom(field.getType()) ;
}
运行结果:

在上面的示例中,我们可以非常简单的通过类型定义和属性名称得到属性的类型。
column属性
column属性用来表示当前result元素所对应的数据库列名称或者数据列的别名,基于和property属性一样的道理,虽然column属性是选填的,但是在作为resultMap元素的直接子元素出现时,column属性是必填的。
假设我们进行如下配置:
<resultMap id="company" type="**.Company">
<result property="boss.name"/>
</resultMap>
因为我们没有指定result元素的column属性,所以我们将会得到一个BuilderException类型的异常,关键异常信息是:
Mapping is missing column attribute for property boss.name
具体的代码可以参见单元测试:ResultMapResultPropertyTest的noColumnTest方法.
详细访问地址:gitee.com/topanda/myb…
jdbcType属性
jdbcType属性是一个非必填的属性,用来表示当前result元素所对应的数据库列的类型,他的作用是用于简化mybatis推断TypeHandler实例的过程。
官方描述为:
JDBC 类型。 只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。这是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 编程,你需要对可以为空值的列指定这个类型。
根据上面的描述,我们可以发现查询语句完全可以不用配置该属性,而增删改语句除非需要操作的数据列是一个空值,否则也不需要配置该属性。
那么为什么在增删改语句中,针对空值列需要配置该属性呢?
抱着这个疑问,第一反应就是查看PreparedStatement对象的setNull()方法:

果然在方法注释中发现了关于sqlType的限制:
-
void setNull (int parameterIndex, int sqlType, String typeName)方法:原文:
<P><B>Note:</B> To be portable, applications must give the SQL type code and the fully-qualified SQL type name when specifying a NULL user-defined or REF parameter. In the case of a user-defined type the name is the type name of the parameter itself. For a REF parameter, the name is the type name of the referenced type. If a JDBC driver does not need the type code or type name information, it may ignore it.译文:
为了便于移植,在指定 NULL 用户定义参数或 REF 参数时,应用程序必须提供 SQL 类型代码和完全限定的 SQL 类型名称。 对于用户定义类型,名称是参数本身的类型名称。 对于 REF 参数,名称是所引用类型的类型名称。 如果 JDBC 驱动程序不需要类型代码和类型名称信息,则可以忽略这些信息。 尽管此方法是供用户定义的参数和 Ref 参数使用的,但也可以使用其设置任何 JDBC 类型的 null 参数。 如果该参数没有用户定义的类型或 REF 类型,则忽略给定的 typeName。 -
void setNull(int parameterIndex, int sqlType)方法:原文:
Note: You must specify the parameter's SQL type.译文:
注:必须指定参数的 SQL 类型。
但是事实上,在mybatis中就算我们的增删改语句操作了一个未指定jdbcType类型的参数,也不会报错,这又是为什么呢?
首先,我们需要明确一点,调用PreparedStatement对象的setNull()方法这一操作,是将java对象转换为数据查询参数,这种由Java类型转换为JdbcType的操作,属于参数映射,而ResultMap对应的结果映射负责的是将JdbcType转换为Java类型,这两个是截然相反的操作。
这意味着什么?这意味着ResultMap的定义永远不会用在参数映射上,因此,ResultMap的定义永远不会触发PreparedStatement对象的setNull()方法,自然不会报错。
所以,jdbcType属性只是用来简化mybatis推断TypeHandler实例的过程。
typeHandler属性
typeHandler属性是一个可选的属性,用于显式声明一个处理当前result元素的TypeHandler实例。
前面已经说过,mybatis可以通过property属性定义对应的java类型推断出处理当前result元素的TypeHandler实例。
那么为什么还要提供一个typeHandler属性呢?
这是因为,有些时候针对一些特殊的字段,我们可能会需要一些特殊的处理操作,比如,对敏感数据脱敏。
这时候我们就需要使用指定的TypeHandler实例取代默认的类型处理器。
通过一个简单的示例来感受一下typeHandler属性的使用。
具体代码可以参见单元测试TypeHandlerTest 的typeHandler方法.
详细访问地址:gitee.com/topanda/myb…
我们新建一个单元测试包org.apache.learning.result_map.type_handler:
在包下新建一个User对象:
import lombok.Data;
@Data
public class User {
private Integer id;
private String name;
private String phone;
}
以及数据库初始化脚本CreateDB.sql,脚本中user表的列定义和User对象的属性一一对应:
drop table user if exists;
create table user
(
id int,
name varchar(20),
phone varchar(20)
);
insert into user (id, name,phone) values (1, 'Panda', '18888888888');
新建一个mybatis-config.xml配置文件,提供了简单的数据源配置:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="" value=""/>
</transactionManager>
<!-- 使用 hsql 内存数据库 -->
<dataSource type="UNPOOLED">
<property name="driver" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:mem:type_handler_test"/>
<property name="username" value="sa"/>
</dataSource>
</environment>
</environments>
</configuration>
然后,编写对应的Mapper定义及其配置文件:
Mapper.java:
public interface Mapper {
/**
* 获取指定ID的用户信息
* @param id ID
* @return 用户信息
*/
User selectById(Integer id);
}
Mapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.learning.result_map.type_handler.Mapper">
<resultMap id="user" type="org.apache.learning.result_map.type_handler.User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="phone" column="phone"
typeHandler="org.apache.learning.result_map.type_handler.PhoneDesensitizationTypeHandler"/>
</resultMap>
<select id="selectById" resultMap="user">
SELECT *
FROM User u
WHERE u.id = #{id}
</select>
</mapper>
需要注意的是,针对于phone属性,我们为其单独指定了PhoneDesensitizationTypeHandler类型转换器。
PhoneDesensitizationTypeHandler是一个简单的类型转换器,他提供了简单的手机号脱敏(前三后四)的功能:
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 手机号脱敏类型转换处理器
*
* @author HanQi [Jpanda@aliyun.com]
* @version 1.0
* @since 2020/4/3 16:21:08
*/
public class PhoneDesensitizationTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter);
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return desensitization(rs.getString(columnName));
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return desensitization(rs.getString(columnIndex));
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return desensitization(cs.getString(columnIndex));
}
private String desensitization(String phone) {
if (StringUtils.isBlank(phone)) {
return phone;
}
return phone.replaceAll("(?<=\\w{3})\\w(?=\\w{4})", "*");
}
}
最后编写一个名为TypeHandlerTest的单元测试类:
import lombok.Cleanup;
import lombok.SneakyThrows;
import org.apache.ibatis.BaseDataTest;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.type.StringTypeHandler;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.Reader;
public class TypeHandlerTest {
private static SqlSessionFactory sqlSessionFactory;
@BeforeEach
@SneakyThrows
public void setup() {
@Cleanup
Reader reader = Resources.getResourceAsReader("org/apache/learning/result_map/type_handler/mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(),
"org/apache/learning/result_map/type_handler/CreateDB.sql");
sqlSessionFactory.getConfiguration().addMapper(Mapper.class);
}
@Test
public void typeHandler() {
@Cleanup
SqlSession sqlSession = sqlSessionFactory.openSession();
Mapper mapper = sqlSession.getMapper(Mapper.class);
User u = mapper.selectById(1);
Assertions.assertEquals("188****8888", u.getPhone());
ResultMap resultMap = sqlSession.getConfiguration().getResultMap("org.apache.learning.result_map.type_handler.Mapper.user");
resultMap.getResultMappings().forEach(r -> {
if ("name".equals(r.getProperty())) {
assert StringTypeHandler.class.isAssignableFrom(r.getTypeHandler().getClass());
}
if ("phone".equals(r.getProperty())) {
assert PhoneDesensitizationTypeHandler.class.isAssignableFrom(r.getTypeHandler().getClass());
}
});
}
}
能够正常运行,这表明我们配置的result元素的typeHandler属性值生效了。
通过上面的单元测试,我们可以发现,虽然name和phone两个属性对应的java类型和jdbc类型都完全一致,但是在运行时,二者使用的TypeHandler实例却不一致。
name属性使用的是默认的StringTypeHandler,phone属性使用的是我们配置的PhoneDesensitizationTypeHandler。
这不仅说明我们为phone属性配置的typeHandler生效了,还表明我们为phone属性配置的TypeHandler实例只作用在phone属性上,没有污染其余的result配置。
总结
到这里,我们就简单的了解了result子元素的属性定义和用法。
下面,我们简单总结一下本章的关键信息:
| 属性名称 | 必填 | 类型 | 描述 |
|---|---|---|---|
| property | false | String | PO对象的属性名称 |
| column | false | String | 数据库中的列名 |
| javaType | false | String | PO对象的属性类型 |
| jdbcType | false | String | 数据库中的列类型 |
| typeHandler | false | String | 负责将数据库数据转换为PO对象的类型转换器 |
关注我,一起学习更多知识
