Mybatis源码之美:3.5.1.解析result元素

872 阅读10分钟

解析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,他的resultproperty属性使用了点式分隔形式,表示将查询到的名称为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以及discriminatorcase的子元素出现,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()方法:

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。
    

    译文取自:tool.oschina.net/uploads/api…

  • void setNull(int parameterIndex, int sqlType)方法:

    原文:

    Note: You must specify the parameter's SQL type.
    

    译文:

    注:必须指定参数的 SQL 类型。
    

    译文取自:tool.oschina.net/uploads/api…

但是事实上,在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属性值生效了。

通过上面的单元测试,我们可以发现,虽然namephone两个属性对应的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对象的类型转换器

关注我,一起学习更多知识

关注我