MybatisPlus multi table connection query one-to-one, one to many, many to many query

1, Preface

(1) Background content

The most common selection component of DAO layer in software application technology architecture is MyBatis. Friends who are familiar with MyBatis know how beautiful MyBatis was in the past. XML files are used to solve the problem of complex database access. Today, the former Dragon Slayer has finally become a dragon. The database access technology based on XML file has become bloated and complex, and the maintenance difficulty has risen sharply.

MybatisPlus encapsulates the common database access, which greatly reduces the dependence on XML files, and developers get a great relief from the bloated XML files.

MybatisPlus does not officially provide a general solution for multi table join query, but join query is a very common demand. There are two requirements for solving connection queries. One is to continue to use MyBatis to provide XML file solutions; Another solution provided in this paper.

In fact, the author strongly recommends to bid farewell to accessing the database through XML and constantly explore new, more friendly and more natural solutions. Now I share the research results of the latest MybatisPlus technology.

<img src="https://www.altitude.xin/typora/image-20211021114957682.png" alt="image-20211021114957682" style="zoom:50%;" />

(2) Scenario description

In order to illustrate the relationship of connection query, here we take students, courses and their relationships as examples.

<img src="https://www.altitude.xin/typora/image-20211020194255298.png" alt="image-20211020194255298" style="zoom:50%;" />

(3) Preliminary preparation

This part requires readers to master the following contents: Lambda expressions, especially method references; Functional interface; Stream operation and so on, otherwise it will be a little difficult to understand.

<img src="https://www.altitude.xin/typora/image-20211021135113431.png" alt="image-20211021135113431" style="zoom:50%;" />

For the mapping relationship between entity class and Vo, the author creatively introduces a special constructor and makes rational use of inheritance relationship, which greatly facilitates developers to complete the transformation from entity class to Vo.

Null pointer exceptions are ignored and not handled with the help of Optional Class implementation, details step by step What's new in Java 8 see.

2, One to one query

The most typical application scenario of one-to-one query is to replace id with name, such as userId with userName.

(1) Query a single record

Querying a single record means that there is only one record with a return value. It is usually a query result returned with a unique index as a condition.

1. Sample code
/**
 * Query individual student information (one student corresponds to one department)
 */
public UserVo getOneUser(Integer userId) {
    LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(User.class)
        .eq(User::getUserId, userId);
    // Query user information first
    User user = userMapper.selectOne(wrapper);
    // Convert to Vo
    UserVo userVo = Optional.ofNullable(user).map(UserVo::new).orElse(null);
    // Query information from other tables and then encapsulate it into Vo
    Optional.ofNullable(userVo).ifPresent(this::addDetpNameInfo);
    return userVo;
}

Supplementary information of subsidiary table

/**
 * Supplementary department name information
 */
private void addDetpNameInfo(UserVo userVo) {
    LambdaQueryWrapper<Dept> wrapper = Wrappers.lambdaQuery(Dept.class)
        .eq(Dept::getDeptId, userVo.getDeptId());
    Dept dept = deptMapper.selectOne(wrapper);
    Optional.ofNullable(dept).ifPresent(e -> userVo.setDeptName(e.getDeptName()));
}
2. Theoretical analysis

Querying a single entity is divided into two steps: querying the main table data according to conditions (null pointer exception needs to be handled); Encapsulate Vo and query attached table data.

The query result (VO) has only one record, and the database needs to be queried twice. The time complexity is O(1).

(2) Query multiple records

Querying multiple records means that the query result is a list, which usually refers to the query result based on the general index.

1. Sample code
/**
 * Batch query of student information (one student corresponds to one department)
 */
public List<UserVo> getUserByList() {
    // Query user information first (in the form of list)
    List<User> user = userMapper.selectList(Wrappers.emptyWrapper());
    List<UserVo> userVos = user.stream().map(UserVo::new).collect(toList());
    // This step can have more than one
    addDeptNameInfo(userVos);
    return userVos;
}

Supplementary information

private void addDeptNameInfo(List<UserVo> userVos) {
    // Extract user userId to facilitate batch query
    Set<Integer> deptIds = userVos.stream().map(User::getDeptId).collect(toSet());
    // Query deptName according to deptId (judge whether it is not empty before querying)
    List<Dept> dept = deptMapper.selectList(Wrappers.lambdaQuery(Dept.class).in(Dept::getDeptId, deptIds));
    // Construct mapping relationship to facilitate matching deptId and deptName
    Map<Integer, String> hashMap = dept.stream().collect(toMap(Dept::getDeptId, Dept::getDeptName));
    // Encapsulate Vo and add it to the collection (key content)
    userVos.forEach(e -> e.setDeptName(hashMap.get(e.getDeptId())));
}
2. Theoretical analysis

First query the list record containing id, extract id from the result set and convert it into batch query statements, then access the database and parse name from the second call result set.

The query result (VO) has multiple records, but only calls the database twice, and the time complexity is O(1).

(3) Query multiple records (pagination)

The idea of paging query entity is similar to that of query list, with additional one-step paging generic conversion.

1. Sample code
/**
 * Paging query of student information (one student corresponds to one department)
 */
public IPage<UserVo> getUserByPage(Page<User> page) {
    // Query user information first
    IPage<User> xUserPage = userMapper.selectPage(page, Wrappers.emptyWrapper());
    // Initialize Vo
    IPage<UserVo> userVoPage = xUserPage.convert(UserVo::new);
    if (userVoPage.getRecords().size() > 0) {
        addDeptNameInfo(userVoPage);
    }
    return userVoPage;
}

Query supplementary information

private void addDeptNameInfo(IPage<UserVo> userVoPage) {
    // Extract user userId to facilitate batch query
    Set<Integer> deptIds = userVoPage.getRecords().stream().map(User::getDeptId).collect(toSet());
    // Query deptName according to deptId
    List<Dept> dept = deptMapper.selectList(Wrappers.lambdaQuery(Dept.class).in(Dept::getDeptId, deptIds));
    // Construct mapping relationship to facilitate matching deptId and deptName
    Map<Integer, String> hashMap = dept.stream().collect(toMap(Dept::getDeptId, Dept::getDeptName));
    // Add query supplementary information to Vo
    userVoPage.convert(e -> e.setDeptName(hashMap.get(e.getDeptId())));
}

The convert method in the IPage interface can be modified on the original instance.

2. Theoretical analysis

First query the list record containing id, extract id from the result set and convert it into batch query statements, then access the database and parse name from the second call result set.

The query result (VO) has multiple records, but only calls the database twice, and the time complexity is O(1).

3, One to many query

The most common scenario of one to many query is to query the student information contained in the Department. Because a Department corresponds to multiple students and each student corresponds to a department, it is called one to many query.

(1) Query a single record

1. Sample code
/**
 * Query a single department (one department has multiple users)
 */
public DeptVo getOneDept(Integer deptId) {
    // Query Department basic information
    LambdaQueryWrapper<Dept> wrapper = Wrappers.lambdaQuery(Dept.class).eq(Dept::getDeptId, deptId);
    DeptVo deptVo = Optional.ofNullable(deptMapper.selectOne(wrapper)).map(DeptVo::new).orElse(null);
    Optional.ofNullable(deptVo).ifPresent(this::addUserInfo);
    return deptVo;
}

Supplementary additional information

private void addUserInfo(DeptVo deptVo) {
    // Query student list according to deptId of Department
    LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(User.class).eq(User::getDeptId, deptVo.getDeptId());
    List<User> users = userMapper.selectList(wrapper);
    deptVo.setUsers(users);
}
2. Theoretical analysis

The whole process is divided into two stages: query the specified Department information through the primary key in the Department table, query the student information through the Department ID foreign key in the student table, and merge the results to form a return value (Vo).

The whole process of one to many querying a single record requires at most two database queries. The query times are constant and the query time complexity is O(1).

(2) Query multiple records

1. Sample code
/**
 * Query multiple departments (one department has multiple users)
 */
public List<DeptVo> getDeptByList() {
    // Query department information by criteria
    List<Dept> deptList = deptMapper.selectList(Wrappers.emptyWrapper());
    List<DeptVo> deptVos = deptList.stream().map(DeptVo::new).collect(toList());
    if (deptVos.size() > 0) {
        addUserInfo(deptVos);
    }
    return deptVos;
}

Supplementary additional information

private void addUserInfo(List<DeptVo> deptVos) {
    // Prepare deptId to facilitate batch query of user information
    Set<Integer> deptIds = deptVos.stream().map(Dept::getDeptId).collect(toSet());
    // Query user information with batch deptId
    List<User> users = userMapper.selectList(Wrappers.lambdaQuery(User.class).in(User::getDeptId, deptIds));
    // Key: group users according to deptId
    Map<Integer, List<User>> hashMap = users.stream().collect(groupingBy(User::getDeptId));
    // Merge results, construct Vo and add set list
    deptVos.forEach(e -> e.setUsers(hashMap.get(e.getDeptId())));
}
2. Theoretical analysis

The whole process is divided into three stages: query several records from the Department table through the general index; Convert the Department ID into batch query and query the student records from the student table; Group student records by department ID, merge the results and convert them into Vo.

To query multiple records one to many, you need to call the database query twice, the query times are constant, and the query time complexity is O(1).

(3) Query multiple records (pagination)

1. Sample code
/**
 * Query department information in pages (one department has multiple users)
 */
public IPage<DeptVo> getDeptByPage(Page<Dept> page) {
    // Query department information by criteria
    IPage<Dept> xDeptPage = deptMapper.selectPage(page, Wrappers.emptyWrapper());
    IPage<DeptVo> deptVoPage = xDeptPage.convert(DeptVo::new);
    if (deptVoPage.getRecords().size() > 0) {
        addUserInfo(deptVoPage);
    }
    return deptVoPage;
}

Query supplementary information

private void addUserInfo(IPage<DeptVo> deptVoPage) {
    // Prepare deptId to facilitate batch query of user information
    Set<Integer> deptIds = deptVoPage.getRecords().stream().map(Dept::getDeptId).collect(toSet());
    LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(User.class).in(User::getDeptId, deptIds);
    // Query user information with batch deptId
    List<User> users = userMapper.selectList(wrapper);
    // Key: group users according to deptId
    Map<Integer, List<User>> hashMap = users.stream().collect(groupingBy(User::getDeptId));
    // Merge results, construct Vo and add set list
    deptVoPage.convert(e -> e.setUsers(hashMap.get(e.getDeptId())));
}
2. Theoretical analysis

The whole process is divided into three stages: query several records from the Department table through the general index; Convert the Department ID into batch query and query the student records from the student table; Group student records by department ID, merge the results and convert them into Vo.

To query multiple records one to many, you need to call the database query twice, the query times are constant, and the query time complexity is O(1).

4, Many to many query

MybatisPlus implementing many to many query is a challenging task and the most difficult part of connection query.

The problem of many to many query is solved by replacing time with space and using stream operation.

Compared with one to many queries, many to many queries add streaming grouping operations, batch HashMap values and other contents.

<img src="https://www.altitude.xin/typora/image-20211024115903848.png" alt="image-20211024115903848" style="zoom:50%;" />

(1) Query a single record

Querying a single record generally refers to finding a record in a matching table through two query criteria.

1. Sample code
public StudentVo getStudent(Integer stuId) {
    // Query student information through primary key
    StudentVo studentVo = ConvertUtils.convertObj(getById(stuId), StudentVo::new);
    LambdaQueryWrapper<StuSubRelation> wrapper = Wrappers.lambdaQuery(StuSubRelation.class).eq(StuSubRelation::getStuId, stuId);
    // Query matching relationship
    List<StuSubRelation> stuSubRelations = stuSubRelationMapper.selectList(wrapper);
    Set<Integer> subIds = stuSubRelations.stream().map(StuSubRelation::getSubId).collect(toSet());
    if (studentVo != null && subIds.size() > 0) {
        List<Subject> subList = subjectMapper.selectList(Wrappers.lambdaQuery(Subject.class).in(Subject::getId, subIds));
        List<SubjectBo> subBoList = EntityUtils.toList(subList, SubjectBo::new);
        HashBasedTable<Integer, Integer, Integer> table = getHashBasedTable(stuSubRelations);
        subBoList.forEach(e -> e.setScore(table.get(stuId, e.getId())));
        studentVo.setSubList(subBoList);
    }
    return studentVo;
}
2. Theoretical analysis

Multiple to multiple single record query can access the database up to 3 times. First query the student information, then query the matching information between the student and the course, and finally query the course score information. The query time complexity is O(1).

(2) Query multiple records

1. Sample code
public List<StudentVo> getStudentList() {
    // Query student information through primary key
    List<StudentVo> studentVoList = EntityUtils.toList(list(), StudentVo::new);
    // Batch query student ID
    Set<Integer> stuIds = studentVoList.stream().map(Student::getId).collect(toSet());
    LambdaQueryWrapper<StuSubRelation> wrapper = Wrappers.lambdaQuery(StuSubRelation.class).in(StuSubRelation::getStuId, stuIds);
    List<StuSubRelation> stuSubRelations = stuSubRelationMapper.selectList(wrapper);
    // Batch query of course ID
    Set<Integer> subIds = stuSubRelations.stream().map(StuSubRelation::getSubId).collect(toSet());
    if (stuIds.size() > 0 && subIds.size() > 0) {
        HashBasedTable<Integer, Integer, Integer> table = getHashBasedTable(stuSubRelations);
        List<Subject> subList = subjectMapper.selectList(Wrappers.lambdaQuery(Subject.class).in(Subject::getId, subIds));
        List<SubjectBo> subjectBoList = EntityUtils.toList(subList, SubjectBo::new);
        Map<Integer, List<Integer>> map = stuSubRelations.stream().collect(groupingBy(StuSubRelation::getStuId, mapping(StuSubRelation::getSubId, toList())));
        for (StudentVo studentVo : studentVoList) {
            // Get course list
            List<SubjectBo> list = ListUtils.select(subjectBoList, e -> emptyIfNull(map.get(studentVo.getId())).contains(e.getId()));
            // Fill fraction
            list.forEach(e -> e.setScore(table.get(studentVo.getId(), e.getId())));
            studentVo.setSubList(list);
        }
    }
    return studentVoList;
}
2. Theoretical analysis

Many to many N records query because batch query is used, the database can be accessed at most 3 times. First query the student information, then query the matching information between the student and the course, and finally query the course score information. The query time complexity is O(1).

(3) Query multiple records (pagination)

1. Sample code
public IPage<StudentVo> getStudentPage(IPage<Student> page) {
    // Query student information through primary key
    IPage<StudentVo> studentVoPage = EntityUtils.toPage(page(page), StudentVo::new);
    // Batch query student ID
    Set<Integer> stuIds = studentVoPage.getRecords().stream().map(Student::getId).collect(toSet());
    LambdaQueryWrapper<StuSubRelation> wrapper = Wrappers.lambdaQuery(StuSubRelation.class).in(StuSubRelation::getStuId, stuIds);
    // Query course scores by Student ID
    List<StuSubRelation> stuSubRelations = stuSubRelationMapper.selectList(wrapper);
    // Batch query of course ID
    Set<Integer> subIds = stuSubRelations.stream().map(StuSubRelation::getSubId).collect(toSet());
    if (stuIds.size() > 0 && subIds.size() > 0) {
        HashBasedTable<Integer, Integer, Integer> table = getHashBasedTable(stuSubRelations);
        // Student ID query course ID group
        Map<Integer, List<Integer>> map = stuSubRelations.stream().collect(groupingBy(StuSubRelation::getStuId, mapping(StuSubRelation::getSubId, toList())));

        List<Subject> subList = subjectMapper.selectList(Wrappers.lambdaQuery(Subject.class).in(Subject::getId, subIds));
        List<SubjectBo> subBoList = EntityUtils.toList(subList, SubjectBo::new);
        for (StudentVo studentVo : studentVoPage.getRecords()) {
            List<SubjectBo> list = ListUtils.select(subBoList, e -> emptyIfNull(map.get(studentVo.getId())).contains(e.getId()));
            list.forEach(e -> e.setScore(table.get(studentVo.getId(), e.getId())));
            studentVo.setSubList(list);
        }
    }
    return studentVoPage;
}
2. Theoretical analysis

Many to many N records paging query because batch query is used, the database can be accessed at most 3 times. First query the student information, then query the matching information between the student and the course, and finally query the course score information. The query time complexity is O(1).

The dependencies that need to be used are used to handle the conversion between entity classes and VO.

<dependency>
    <groupId>xin.altitude.cms.common</groupId>
    <artifactId>ucode-cms-common</artifactId>
    <version>1.3.3</version>
</dependency>

5, Summary and expansion

(1) Summary

Through the above analysis, we can use MybatisPlus to solve one-to-one, one to many and many to many queries in multi table connection queries.

  • The above code is compact and makes full use of the IDE's support for Lambda expressions to complete the check of the code during compilation.
  • The business logic is clear, and the advantages of maintainability and modifiability are obvious.
  • One query needs to access the database at most twice. The time complexity is o(1). Primary key query or index query has high query efficiency.

(2) Expand

MybatisPlus can well solve the problem of single table query, and can well solve the problem of connection query with the help of the encapsulation of single table query.

This scheme not only solves the problem of connection query, but also has the following expansion:

  • When the amount of data is large, it still has stable query efficiency

When the amount of data reaches the level of one million, the traditional single table query through index has faced challenges. The performance of ordinary multi table join query decreases exponentially with the increase of the amount of data.

In this scheme, the join query is transformed into a primary key (index) query, and the query performance is equivalent to that of a single table query.

  • Use with L2 cache to further improve query efficiency

When all queries are converted to single table based queries, the secondary cache can be safely introduced. The single table addition, deletion, modification and query operation of the secondary cache is self-adaptive linkage, which solves the problem of dirty data in the secondary cache.

I like this article ♥️ fabulous ♥️ Support me. Pay attention to me. See you next time. The relevant source code is in GitHub , video explanation Station B , collected in Special blog.

Keywords: Spring Boot mybatis-plus

Added by jdavidbakr on Thu, 24 Feb 2022 02:26:45 +0200