Mybatis source detailed series -- mybatis usage and details you don't know

brief introduction

This is Mybatis In the fourth part of the blog series, I intended to explain the configuration, mapper and dynamic sql of mybatis in detail, but Official Chinese document of Mybatis The introduction of this part has been detailed enough, which can be referred to directly if necessary. So, I'll extend some other features or use details, and master them to use mybatis more elegantly and efficiently.

To add a little bit, all the test examples in this article are based on this series Mybatis For the first article, other related blogs are as follows:

Mybatis source code explanation series (1) -- what the persistence layer framework solves and how to use mybatis

Mybatis source details series (2) -- how to load configuration and initialization for mybatis

Mybatis source code explanation series (3) -- Viewing the execution logic of mybatis from the Mapper interface

Powerful result handler -- ResultHandler

DO to VO -- common method

In general, the objects in our persistence layer will not (should not) respond directly to the caller, and need to be converted into VO objects and then respond. Based on the use examples of this series of blogs, let's assume that I need to return the following VO objects in the web layer, as follows. In this class, in addition to the fields of employee table, the fields of department table are also included.

public class EmployeeVO implements Converter<Employee, EmployeeVO>, Serializable {

    private static final long serialVersionUID = 1L;

    private String id;

    private String name;

    private String genderStr;

    private String no;

    private String password;

    private String phone;

    private String address;

    private Byte status;

    private String departmentId;
    
    private String departmentName;
    
    private String departmentNo;
    
        @Override
    public EmployeeVO convert(Employee value) {
        EmployeeVO employeeVO = new EmployeeVO();
        BeanUtils.copyProperties(employeeVO, value);
        employeeVO.setGenderStr(value.getGender()?"male":"female");
        Department department = value.getDepartment();
        if(department != null) {
            employeeVO.setDepartmentName(department.getName());
            employeeVO.setDepartmentNo(department.getNo());
        }
        return employeeVO;
    }
    
    // Omit other methods
}

The operation of the web layer is roughly like this. I first query out the collection of employees, and then perform object conversion.

    @RequestMapping("/getList")
	public ResponseData testResultHandler(@RequestBody EmployeeCondition con) {
        List<Employee> list = employeeService.list(con);
        return ResultDataUtil.getResultSucess(ConvertUtil.convertList(list, new EmployeeVO()));
    }

DO to VO--ResultHandler method

When using Mybatis, there is actually another solution to deal with the problem of DO to VO, which is to use the result processor -- ResultHandler, as follows.

public interface ResultHandler<T> {
  void handleResult(ResultContext<? extends T> resultContext);
}

This is an interface. The implementation class needs to be defined by ourselves. As an example of a test, I've simply defined one here.

public class MyResultHandler<T, R> implements ResultHandler<T> {
    private List<R> list = new ArrayList<R>();
    private Converter<T, R> converter;
    
    public MyResultHandler(Converter<T, R> converter) {
        this.converter = converter;
    }
    
    @Override
    public void handleResult(ResultContext<? extends T> resultContext) {
        list.add(ConvertUtils.convertObject(resultContext.getResultObject(), converter));
    }
    
    public List<R> getList(){
        return list;
    }
}

When using ResultHandler, the method definition of Mapper interface needs to be adjusted, the input parameter needs to be passed into ResultHandler, and the return value must be void. As for the method content corresponding to xml, it is the same as the common method and does not need to be changed. There is no problem for the following two methods to share an xml select node. Don't worry about that.

    // Common ways
	List<Employee> selectByCondition(@Param("con") EmployeeCondition con);	
	// How to ResultHandler
	void selectByCondition(@Param("con") EmployeeCondition con, ResultHandler<Employee> resultHandler);

Finally, back to our web layer, the code of service layer is ignored. When calling the service layer, I have got the converted VO object, and I don't need to deal with it.

    @RequestMapping("/getList")
	public ResponseData testResultHandler(@RequestBody EmployeeCondition con) {
         MyResultHandler<Employee, EmployeeVO> resultHandler = new MyResultHandler<>(new EmployeeVO());
        employeeService.list(con, resultHandler);
        return ResultDataUtil.getResultSucess(resultHandler.getList());
    }

The last blog mentioned this interface in the process of source code analysis. When the method input parameter of Mapper interface contains ResultHandler and the return type is void, Mybatis Special handling for this situation: when traversing the result set for mapping, every object mapped will call the ResultHandler once and pass the mapped object in. At this time, we can handle the object at will, including our common DO to VO. Of course, its function is not limited to this.

Paging does not require plug-ins -- RowBounds

In this series of usage articles, it is mentioned that pagehelper is used to support paging function. In essence, plug-ins are used to embed paging parameters into sql. In fact, Mybatis has provided RowBounds to support paging function, which does not need to install plug-ins. In essence, MybatisPlus uses this method.

Like the ResultHandler, we only need to modify the Mapper interface method as follows.

    List<Employee> selectByCondition(@Param("con") EmployeeCondition con, RowBounds rowBounds);

Here I simply write a test class and use the RowBounds object directly. In fact, it's better to wrap RowBounds more.

    /**
     * <p>Test rowbounds</p>
     */
    @Test
    public void testRowBounds() {
        EmployeeCondition con = new EmployeeCondition();
        // Set conditions
        con.setAddress("Beijing");

        // Execute, get employee object
        RowBounds rowBounds = new RowBounds(1, 4);
        List<Employee> list = employeeRepository.list(con, rowBounds);

        // Printing
        list.forEach(System.out::println);
    }

Test the above code, and you can see that the typed statement is embedded with paging parameters:

SELECT e.id, e.`name`, e.gender, e.no, e.password
	, e.phone, e.address, e.status, e.deleted, e.department_id
	, e.gmt_create, e.gmt_modified
FROM demo_employee e
WHERE 1 = 1
	AND e.address = ?
LIMIT ?, ?

Is this easier than using plug-ins?

Delay loading

Review the content of the use article

We know that nested select queries are used in resultMap, and lazy loading is used for global declaration, so nested properties can be loaded on demand.

    <settings>
        <setting name="lazyLoadingEnabled" value="true" />
    </settings>

Return to the example in the usage section. mapper is configured as follows. Employee objects are associated with departments (one-to-one), roles (one to many), and menus (one to many):

    <!-- Base mapping tables: nesting Select Query mapping -->
    <resultMap id="BaseResultMap" type="Employee">
        <id column="id" property="id" javaType="string" jdbcType="VARCHAR" />
        <result column="department_id" property="departmentId" javaType="string" jdbcType="VARCHAR" />
        <result column="gmt_create" property="create" javaType="date" jdbcType="TIMESTAMP" />
        <result column="gmt_modified" property="modified" javaType="date" jdbcType="TIMESTAMP" />
        <association property="department" 
            column="department_id"
            select="cn.zzs.mybatis.mapper.DepartmentMapper.selectByPrimaryKey" />
        <collection property="roles" 
            column="id" 
            select="cn.zzs.mybatis.mapper.RoleMapper.selectByEmployeeId" />
        <collection property="menus" 
            column="id" 
            select="cn.zzs.mybatis.mapper.MenuMapper.selectByEmployeeId" />
    </resultMap>
    <!-- according to id query-->
    <select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"></include>
        from
        demo_employee e
        where
        e.id = #{id,jdbcType=VARCHAR}
    </select>

In the test code, we comment out the code of points 1, 3 and 4, that is, only the getDepartment() method is called.

    /**
     * <p>Test lazy load trigger</p>
     */
    @Test
    public void testGetLazy() {
        // Set the output proxy class to the specified path
        // -Dcglib.debugLocation=D:/growUp/test
        String id = "cc6b08506cdb11ea802000fffc35d9fe";

        // Execute, get employee object
        Employee employee = employeeRepository.get(id);
        
        // 1. Print employees
        // System.out.println(employee);
        // 2. Printing Department
        System.out.println(employee.getDepartment());
        // 3. Print role
        // employee.getRoles().forEach(System.out::println);
        // 4. Print menu
        // employee.getMenus().forEach(System.out::println);

    }

Testing the above code, we can see that only departments are loaded, but roles and menus are not, which well implements the on-demand loading.

Then let's let go of the first point, that is, to add printing staff. Note that I did not override the toString() method in the use example, so the associated object will not be used in the method.

    @Test
    public void testGetLazy() {
        // Set the output proxy class to the specified path
        // -Dcglib.debugLocation=D:/growUp/test
        String id = "cc6b08506cdb11ea802000fffc35d9fe";

        // Execute, get employee object
        Employee employee = employeeRepository.get(id);
        System.out.println("================");
        
        // 1. Print employees
        System.out.println(employee);
        // 2. Printing Department
        // System.out.println(employee.getDepartment());
        // 3. Print role
        // employee.getRoles().forEach(System.out::println);
        // 4. Print menu
        // employee.getMenus().forEach(System.out::println);

    }

Testing the above code, we were surprised to find that at this time, departments, roles and menus were printed out. What about the agreed on-demand loading?

It's strange that the methods I call don't use associated objects. Why are they loaded?

When to trigger delayed loading

In the above example, did our on-demand load fail?

In fact, no, for Mybatis, it can know that methods such as getDepartment() will use associated objects, but methods such as toString() cannot. Considering that we will use nested objects when overriding toString method, Mybatis will trigger delayed loading by default. In the same way, the methods such as equals(),clone(),hashCode() are the same. Pay attention to equals() and hashCode() in the project.

So, how do we control this behavior? Mybatis provides the lazyLoadTriggerMethods configuration item to specify which methods of the object trigger the delayed load:

Set name describe Effective value Default
lazyLoadTriggerMethods Specifies which methods trigger the loading of all deferred load properties of the object. Comma separated list of methods. equals,clone,hashCode,toString

We will modify the configuration as follows:

<setting name="lazyLoadingEnabled" value="true" />
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode" />

Test the above example again. At this time, none of the nested objects are loaded.

In addition, another configuration item, aggressiveLazyLoading, will also affect the trigger of delayed loading. After 3.4.1, this configuration item will remain the default. If it is not necessary, it is strongly recommended not to configure it as true. If you configure aggressiveLazyLoading to true, all nested objects will be loaded even if you are just getId().

Set name describe Effective value Default
aggressiveLazyLoading When on, calls to almost any method load all of the object's lazy load properties.
Otherwise, each delayed load property is loaded on demand.
true | false false (true by default in versions 3.4.1 and earlier)

As a summary of the delay loading part, here we compare the effects of different combinations of configuration items:

aggressiveLazyLoading lazyLoadTriggerMethods Effect
true / If any method, equals, clone, hashCode, toString in Employee class is called, delay plus will be triggered
false equals,clone,hashCode,toString When the getter method, equals, clone, hashCode and toString of the associated object in the Employee class are called, the delayed loading will be triggered
false equals When the getter method and equals of the associated object in the Employee class are called, delayed loading will be triggered

Some delays? Some without delay

If I want some of the associated objects to be loaded without delay, some of them need to be loaded again. For example, when querying employee objects, the Department will find them, and the roles will be loaded when they need to be used. In this case, you can use fetchType in the mapping relationship to override the switch state of delayed loading:

		<association property="department" 
            column="department_id"
            fetchType="eager" 
            select="cn.zzs.mybatis.mapper.DepartmentMapper.selectByPrimaryKey" />
        <collection property="roles" 
            column="id" 
            select="cn.zzs.mybatis.mapper.RoleMapper.selectByEmployeeId" />
        <collection property="menus" 
            column="id" 
            select="cn.zzs.mybatis.mapper.MenuMapper.selectByEmployeeId" />

A big hole of nested result mapping

In the usage part, I said that if it is collection in nested results, there will be problems with the total number of pages. Therefore, the best way to nest result mapping is only for association.

At that time, I didn't explain the specific reasons. Let me add here.

Total number of errors

Or go back to the example in the usage section. mapper's resultMap is configured as follows:

    <!-- Base mapping table: nested result mapping-->
    <resultMap id="BaseResultMap2" type="Employee" autoMapping="true">
        <id column="id" property="id" javaType="string" jdbcType="VARCHAR" />
        <result column="department_id" property="departmentId" javaType="string" jdbcType="VARCHAR" />
        <result column="gmt_create" property="create" javaType="date" jdbcType="TIMESTAMP" />
        <result column="gmt_modified" property="modified" javaType="date" jdbcType="TIMESTAMP" />
        <association property="department" 
            columnPrefix="d_"
            resultMap="cn.zzs.mybatis.mapper.DepartmentMapper.BaseResultMap" />
    </resultMap>

Write the test method as follows. Here, the pagehelper plug-in will be used to count the total number of queries and paginate. If RowBounds is used, the test results will not be affected. Note that "zzs001" in the database has only one record, and the total number of queries and mapping objects will be one.

    @Test
    public void testlistPage() {
        EmployeeCondition con = new EmployeeCondition();
        // Set conditions
        con.setName("zzs001");
        con.setJoinDepartment(true);
        // con.setJoinRole(true); / / this comment will be released later
        
        // Set paging information
        PageHelper.startPage(0, 3);

        // Execute query
        List<Employee> list = employeeRepository.list2(con);
        // Ergodic result
        list.forEach(System.out::println);

        // Encapsulate paging model
        PageInfo<Employee> pageInfo = new PageInfo<>(list);

        // Take data of paging model
        System.out.println(Long.valueOf(pageInfo.getTotal()).intValue() == list.size());
    }

Test the code, you can see that the total number and the actual number of paging statistics will be the same, no problem at all.

Next, I add a nested object of type collection to resultMap.

    <!-- Base mapping table: nested result mapping-->
    <resultMap id="BaseResultMap2" type="Employee" autoMapping="true">
        <id column="id" property="id" javaType="string" jdbcType="VARCHAR" />
        <result column="department_id" property="departmentId" javaType="string" jdbcType="VARCHAR" />
        <result column="gmt_create" property="create" javaType="date" jdbcType="TIMESTAMP" />
        <result column="gmt_modified" property="modified" javaType="date" jdbcType="TIMESTAMP" />
        <association property="department" 
            columnPrefix="d_"
            resultMap="cn.zzs.mybatis.mapper.DepartmentMapper.BaseResultMap" />
        <collection property="roles" 
            columnPrefix="r_" 
            resultMap="cn.zzs.mybatis.mapper.RoleMapper.BaseResultMap" />
    </resultMap>

Release the comments in the test code and test as follows. One mapping object, yes, but the total number of queries is 2???

This is a big hole in nested result mapping that I mentioned.

Cause analysis

Is the statistics wrong? Let's execute the sql of the console. There are two records. Where did they come out???

In fact, the root cause is that the nested result mapping of collection should not be used in scenarios involving statistics. Our sql as like as two peas, two of which are checked out. We will find out that the id of these two records is exactly the same. We will find out 1 fields.

It should be understood that the statistical error is mainly caused by the join table. Employees and roles are one to many relationships. When employees have multiple roles, more records than employees will appear in the joint table query. These records will be merged when Mybatis maps objects.

This leads to the so-called total number of errors. Therefore, nested result mapping of collection is not suitable for statistical scenarios.

Automatic mapping

Turn on automatic mapping

The result auto mapping of mybatis is on by default. It can be modified by using the setting configuration item. It has three auto mapping levels:

  • NONE - disables automatic mapping. Only manually mapped properties are mapped.
  • PARTIAL - maps properties other than the nested result map (that is, connected properties) defined internally. Default configuration.
  • FULL - automatically map all attributes.

PARTIAL is used by default. In addition, no matter what level of automatic mapping is set, you can enable / disable automatic mapping for the specified result mapping setting by setting the autoMapping property of resultMap in the mapping file.

    <resultMap id="BaseResultMap2" type="Employee" autoMapping="true">
        <id column="id" property="id" javaType="string" jdbcType="VARCHAR" />
    </resultMap>

Automatic mapping of hump naming attributes

When the query results are mapped automatically, MyBatis takes the column names returned in the results and looks up the properties with the same names in the Java class (ignoring case). If the column name does not match the property name in the entity, you need to explicitly configure it. In the example, we use resultMap to map tables and objects, as follows:

    <!-- Basic mapping table -->
    <resultMap id="BaseResultMap" type="cn.zzs.mybatis.entity.Employee">
        <id column="id" property="id" javaType="string" jdbcType="VARCHAR" />
        <result column="department_id" property="departmentId" javaType="string" jdbcType="VARCHAR" />
        <result column="gmt_create" property="create" javaType="date" jdbcType="TIMESTAMP" />
        <result column="gmt_modified" property="modified" javaType="date" jdbcType="TIMESTAMP" />
    </resultMap>
    <!-- Basic fields -->
    <sql id="Base_Column_List">
        e.id,
        e.`name`,
        e.gender,
        e.no,
        e.password,
        e.phone,
        e.address,
        e.status,
        e.deleted,
        e.department_id,
        e.gmt_create,
        e.gmt_modified
    </sql>
    <!-- according to id query -->
    <select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from
        demo_employee e
        where
        e.id = #{id}
    </select>

In addition to the fact that the table column name is consistent with the entity property name, we need to manually configure the mapping for other fields, which is cumbersome. However, in most cases, we will follow the hump naming rule to define the attribute name of an entity. Can we map it directly through this rule?

mybatis provides the mapUnderscoreToCamelCase configuration item to handle this situation.

    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true" />
    </settings>

reference material

Official Chinese document of Mybatis

Please move to related source code: mybatis-demo

This is an original article. For reprint, please attach the original source link: https://www.cnblogs.com/ZhangZiSheng001/p/12773971.html

Tags: Mybatis SQL Java xml

Posted on Sat, 25 Apr 2020 21:58:23 -0700 by alsal