[MyBatis Series 4] One-to-one, one-to-many, many-to-many query and lazy loading (N+1 problem) analysis

Preface

The last article analyzed the use of configuration in MyBatis, and the dynamic tag function in MyBatis is also very powerful. This article will not introduce all tags, mainly for resultMap to introduce how to use sql tags to configure dynamic sql for complex queries.

Fixed parameter query

First, let's look at how to implement a query statement with fixed parameters: The
following two methods are added to UserMapper.java:

 List<LwUser> listUserByUserName(@Param("userName") String userName);

 List<LwUser> listUserByTable(@Param("tableName") String tableName);

The corresponding sql statement in UserMapper.xml is:

  <select id="listUserByUserName" resultType="lwUser">
        select user_id,user_name from lw_user where user_name=#{
    
    userName}
    </select>

    <select id="listUserByTable" resultType="lwUser">
        select user_id,user_name from ${
    
    tableName}
    </select>

Then execute the query:

package com.lonelyWolf.mybatis;

import com.alibaba.fastjson.JSONObject;
import com.lonelyWolf.mybatis.mapper.UserMapper;
import com.lonelyWolf.mybatis.model.LwUser;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class MyBatisQueryByParam {
    
    
    public static void main(String[] args) throws IOException {
    
    
        String resource = "mybatis-config.xml";
        //读取mybatis-config配置文件
        InputStream inputStream = Resources.getResourceAsStream(resource);
        //创建SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //创建SqlSession对象
        SqlSession session = sqlSessionFactory.openSession();

        /**
         * 相比较于session.selectList("com.xxx.UserMapper.listAllUser")来实现查询,
         * 下面这种通过先获取mapper再挑用mapper中方法的方式会更灵活
         */
        UserMapper userMapper = session.getMapper(UserMapper.class);
        List<LwUser> userList = userMapper.listUserByUserName("孤狼1号");

        System.out.println(null == userList ? "": JSONObject.toJSONString(userList));

        List<LwUser> userList2 = userMapper.listUserByTable("lw_user");
        System.out.println(null == userList2 ? "": JSONObject.toJSONString(userList2));
    }
}

The query result output is as follows:
Insert picture description here

# And $ difference

From the screenshot of the output sql statement above, you can see that if you use #, then the sql statement will first use a placeholder in the sql statement, that is, pre-compilation, corresponding to the PreparedStatement in JBDC. And using $, will directly spell the parameters to the SQL statement, which is equivalent to Statement in JDBC.

In general, it is not recommended to use $, because this direct splicing method is easy to be attacked by SQL injection.
For example, the sql statement above:

select user_id,user_name from ${tableName}

If the tableName passed in is: lw_user; delete from lw_user; then the SQL statement executed at this time will become:

select user_id,user_name from lw_user;delete from lw_user;

At this time, the data of the entire table will be deleted, and if #{tableName} is used, the following sql is finally executed:

select user_id,user_name from 'lw_user;delete from lw_user;'

The result is only to query a non-existent table.

Dynamic parameter query

The parameters in the above example are fixed, what if our parameters are not fixed? For example, there are two parameters, but I may not use one, or only use one, or use both. How can this be achieved?
As shown in the figure below, you can use the where and if tags in combination, and both conditions are written and, this is because Mybatis will help us deal with the redundant and keywords.

<select id="list" parameterType="com.lonelyWolf.mybatis.model.LwUser" resultType="lwUser">
        select user_id,user_name from lw_user
        <where>
            <if test="userId !=null and userId !=''">
                and user_id=#{userId}
            </if>
            <if test="userName !=null and userName !=''">
                and user_name=#{userName}
            </if>
        </where>
    </select>

In other words, we need to splice different sql with different values ​​for the same parameter, then we can splice different sql according to different parameters through the choose tag

 select user_id,user_name from lw_user
        <where>
            <choose>
                <when test="userId ='1'">
                    and user_id=#{userId}
                </when>
                <when test="userId='2'">
                    and user_id=#{userId}
                </when>
                <otherwise>
                    and user_id=#{userId}
                </otherwise>
            </choose>
        </where>
    </select>

Of course, Mybatis also provides many other tags to handle more complex combinations, so I won’t give an example here.

One to one query

If we now have two tables with a one-to-one relationship, we want to query them at the same time. Of course, the easiest way is to write another class and put the result attributes of the two tables into one class, but this way is undoubtedly It will cause a lot of duplication of code, and it does not reflect the hierarchical relationship. If we have a table lw_user table that stores user information, and another table lw_user_job stores the user's work experience, then it is obvious that the job corresponding class should be included in the user class Inside, how should this be achieved?

Please see!

1. Create a new entity class UserJob to map the attributes of the lw_user_job table:

package com.lonelyWolf.mybatis.model;

public class LwUserJob {
    
    
    private String id;
    private String userId; //用户id
    private String companyName; //公司名
    private String position; //职位

    public String getId() {
    
    
        return id;
    }

    public void setId(String id) {
    
    
        this.id = id;
    }

    public String getUserId() {
    
    
        return userId;
    }

    public void setUserId(String userId) {
    
    
        this.userId = userId;
    }

    public String getCompanyName() {
    
    
        return companyName;
    }

    public void setCompanyName(String companyName) {
    
    
        this.companyName = companyName;
    }

    public String getPosition() {
    
    
        return position;
    }

    public void setPosition(String position) {
    
    
        this.position = position;
    }
}

2. Add a reference attribute to the original LwUser class to reference LwUserJob:

package com.lonelyWolf.mybatis.model;

public class LwUser {
    
    
    private String userId; //用户id
    private String userName; //用户名称
    
    private LwUserJob usreJobInfo;//用户工作信息

    public String getUserId() {
    
    
        return userId;
    }

    public void setUserId(String userId) {
    
    
        this.userId = userId;
    }

    public String getUserName() {
    
    
        return userName;
    }

    public void setUserName(String userName) {
    
    
        this.userName = userName;
    }

    public LwUserJob getUsreJobInfo() {
    
    
        return usreJobInfo;
    }

    public void setUsreJobInfo(LwUserJob usreJobInfo) {
    
    
        this.usreJobInfo = usreJobInfo;
    }
}

3. A new method is added to UserMapper.java:

List<LwUser> listUserAndJob();

At this time, UserMapper.xml needs to customize a ResultMap:

<resultMap id="JobResultMap" type="lwUser">
        <result column="user_id" property="userId" jdbcType="VARCHAR" />
        <result column="user_name" property="userName" jdbcType="VARCHAR" />
        <!--这里的JavaType也可以定义别名;property对应LwUser类中的属性名 -->
        <association property="userJobInfo" javaType="com.lonelyWolf.mybatis.model.LwUserJob">
            <result column="id" property="id" jdbcType="VARCHAR" />
            <result column="company_Name" property="companyName" jdbcType="VARCHAR" />
            <result column="position" property="position" jdbcType="VARCHAR" />
        </association>
    </resultMap>

<!-- 这里需要修改为resultMap-->
 <select id="listUserAndJob" resultMap="JobResultMap">
        select * from lw_user u inner join lw_user_job j on u.user_id=j.user_id
    </select>

At this time, execute the query to get the following results:

[{
    
    "userId":"1","userJobInfo":{
    
    "companyName":"自由职业","id":"11","position":"初级开发"},"userName":"孤狼1号"}]

One-to-many query

Assuming the one-to-many relationship between user information and work information, what should we do?
Only need to do 2 simple transformations:
1. Change the reference attribute in LwUser to List:

private List<LwUserJob> userJobList;

2. The ResultMap file in the Mapper was modified at the same time, replacing the association tag with the collection tag, and at the same time modifying the javaType to ofType:

 <collection property="userJobList" ofType="com.lonelyWolf.mybatis.model.LwUserJob">
            <result column="id" property="id" jdbcType="VARCHAR" />
            <result column="company_Name" property="companyName" jdbcType="VARCHAR" />
            <result column="position" property="position" jdbcType="VARCHAR" />
        </collection>

Execute the query again and get the following results:

[{
    
    "userId":"1","userJobList":[{
    
    "companyName":"自由职业","id":"11","position":"初级开发"}],"userName":"孤狼1号"}]

You can see that the userJobList at this time is already an array.

PS: I remember that someone asked before if the attribute mapping must map all the attributes in the table. The answer is no. If you need to map a few, you don’t need to map it completely.

Many-to-many query

Many-to-many is actually similar to one-to-many. It uses the collection tag, that is, nesting the collection tag inside the collection tag can realize the many-to-many query. I will not give an example here.

Lazy loading (solving N+1 problem)

Let's first look at another way of writing one-to-many, which is to support a nested query:

 <resultMap id="JobResultMap2" type="lwUser">
        <result column="user_id" property="userId" jdbcType="VARCHAR" />
        <result column="user_name" property="userName" jdbcType="VARCHAR" />

        <collection property="userJobList" ofType="com.lonelyWolf.mybatis.model.LwUserJob" column="user_id" select="selectJob">
        </collection>
    </resultMap>

<!-- 外部查询-->
  <select id="selectUserAndJob" resultMap="JobResultMap2">
        select * from lw_user
    </select>

<!-- 嵌套查询-->
    <select id="selectJob" resultType="com.lonelyWolf.mybatis.model.LwUserJob">
        select * from lw_user_job where user_id=#{
    
    userId}
    </select>

There are no attributes defined in the above collection, but two tags are defined in the collection, which represent the meaning of passing the value user_id of the current query result to the query selectJob. We define a method to execute this external query selectUserAndJob to see what the results will be:
Insert picture description here
you can see how many pieces of data in the external query will trigger the internal query several times, which is the N+1 problem caused by nested queries . (This problem also exists when using the association tag)

This is not allowed in scenarios with high performance requirements, which is a waste of resources, and MyBatis officials do not recommend us to use this method.

Solve the N+1 problem

Although MyBatis does not recommend us to use this kind of nested query, it also provides a way to solve the N+1 problem, that is, when we execute the external query, the internal query will not be triggered, only when we use the internal object Will trigger the internal query to obtain the corresponding results, this is called lazy loading .

Delayed loading needs to be controlled by global properties, which is disabled by default.
Let's try to enable delayed loading in the mybatis-config.xml file:

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

Then we will execute the following statement:

List<LwUser> userList = userMapper.selectUserAndJob();
        System.out.println(userList.size());//不会触发
        System.out.println(userList.get(0).getUserJobList());//触发
        System.out.println(null == userList ? "": JSONObject.toJSONString(userList));//触发

The output is:
Insert picture description here

Lazy loading principle

Lazy loading actually uses a dynamic proxy to generate a new object. The default is the Javassist dynamic proxy, which can be controlled by parameters and supports switching to CGLIB:

<setting name="proxyFactory" value="CGLIB" />

to sum up

This article mainly describes how to use MyBatis to achieve one-to-one, one-to-many and many-to-many queries, and explains how to use lazy loading to solve the N+1 problem in nested queries. The first two articles in the MyBatis series are relatively basic and do not have an in-depth analysis of the implementation principles. They only explain how to use them. Starting from the next article, I will analyze the source code of MyBatis and some advanced features such as sqlSession execution process, cache, parameters and result sets. The realization principle of functions such as mapping.
Please pay attention to me and learn and progress with the lone wolf .

Guess you like

Origin blog.csdn.net/zwx900102/article/details/108559220