mybatis映射文件相关的知识点总结

发布于:2025-03-05 ⋅ 阅读:(14) ⋅ 点赞:(0)

mybatis映射文件相关的知识点总结

mybatis官网地址

英文版:https://mybatis.org/mybatis-3/index.html

中文版:https://mybatis.p2hp.com/

搭建环境

/*
SQLyog Ultimate v10.00 Beta1
MySQL - 8.0.30 : Database - mybatis-label
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`mybatis-label` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;

USE `mybatis-label`;

/*Table structure for table `user` */

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

/*Data for the table `user` */

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
package com.yimeng.config;

import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
//@MapperScan("com.yimeng.**.mapper")// 注册mapper接口的bean的方式1:扫描mapper接口的包注册bean
public class MybatisConfig {

    // 注册mapper接口的bean的方式2:注册MapperScannerConfigurer,扫描mapper接口的包注册bean
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("com.yimeng.**.mapper");
        return mapperScannerConfigurer;
    }
}
package com.yimeng.domain;

import lombok.Data;

@Data
public class User {
    private int id;
    private String userName;
    private String password;
}
package com.yimeng.mapper;

import com.yimeng.domain.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

//@Mapper// 注册mapper接口的bean的方式3:在对应的接口上添加@Mapper注解
public interface UserMapper {
    public List<User> findAll();
}
package com.yimeng.service.impl;

import com.yimeng.domain.User;
import com.yimeng.mapper.UserMapper;
import com.yimeng.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {
    @Resource
    private UserMapper userMapper;

    @Override
    public List<User> findAll() {
        return userMapper.findAll();
    }
}
package com.yimeng.service;

import com.yimeng.domain.User;
import java.util.List;

public interface UserService {
    public List<User> findAll();
}
package com.yimeng;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringbootMybatisLabelApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootMybatisLabelApplication.class, args);
    }
}
<?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="com.yimeng.mapper.UserMapper">

    <!--查询操作-->
    <select id="findAll" resultType="user">
        select * from user
    </select>
</mapper>
<?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>
    <!-- 日志实现 -->
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
</configuration>
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatis-label
    username: root
    password: 815924
    type: com.alibaba.druid.pool.DruidDataSource

mybatis:
  # 加载全局的配置文件(mapper的xml可以通过yaml指定加载,但是mapper接口得在配置类中使用@MapperScan("com.yimeng.**.mapper")或者注册MapperScannerConfigurer或者在借口上面使用@Mapper注解)
  config-location: classpath:mybatis/mybatis-config.xml
  # 加载映射文件
  mapper-locations: classpath:mapper/*.xml
  # 指定别名包
  type-aliases-package: com.yimeng.domain
package com.yimeng;

import com.yimeng.domain.User;
import com.yimeng.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;

@SpringBootTest
public class SpringbootMybatisApplicationTests {
    @Resource
    private UserService userService;

    @Test
    void findAll() {
        List<User> userList = userService.findAll();
        System.out.println("查询到的数据:"+userList);
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
    </parent>

    <groupId>org.example</groupId>
    <artifactId>mybatis-label</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>
    </dependencies>
</project>

执行结果:

image-20241225115145484

数据库内容:

image-20241225115308635

知识铺垫

SQL映射文件的基本结构

一个完整的 Mapper 映射文件,需要有约束头 xml 与 !DOCTYPE ,其次才是 mapper 根元素,最后再是顶级元素(顶级元素就是在mapper元素下的直接子元素)。

还有就是mapper.xml的命名空间namespace要和接口的全限定名匹配,因为这样才能使用mybatis的动态代理,就可以不用写实现类了。这个前面的文章中已经说过了,这个不细讲了。

如下:

image-20250104101148353

在项目里面每一个【命名空间标识 + 语句块 ID】都是唯一的,不能重复的。

问答
  1. 两个xml可以定义为一个namespace吗?

    答:可以。但是,【命名空间标识 + 语句块 ID】是唯一的。

  2. 使用mybatis时对mapper.xml的文件名有要求吗?

    答:没有。xml的文件名其实怎么写都行,不会影响运行,但是最好还是按照开发规范来命名,比如叫XXXMapper.xml。为什么不影响呢?其实是因为生成动态代理是通过【要代理的mapper接口+方法名】,找到【namespace+ID】对应的sql,然后生成具体实现的。

  3. 通过第一点和第二点,是不是可以说明,我定义一个mapper接口,mapper中有2个方法,mapper中的2个方法,分别使用一个namespace(和mapper接口全限定类名对应)的任意文件名的两个xml文件中,这样的情况下,我们的动态代理仍然能找到对应sql,并生成正确的实现给mapper接口的方法作为实现。总之,不管mapper.xml的文件名,所有能被扫描到的mapper.xml只要namespace一样,都会被放到一个空间里面去。mybatis的动态代理是接口的全限定类名,然后去对应的namespace空间里找id和方法名对上的sql,然后生成实现的。

    答:理解正确。

理解
  1. 注入一个Mapper,这个Mapper注入地方写了一个接口,并且接口没有实现类,但是你可以直接调用注入对象的方法,原因mybatis集成spring后,可以通过注入,直接拿到mybatis为mapper接口生成的代理对象。代理对象实现了Mapper接口的所有方法,具体实现的功能你需要通过「 接口的权限定类名+方法名 」找到对应的「 命名空间标识 + 语句块 ID 」指定元素的sql,其中注意没有要求mapper.xml的文件名,并且可以多个mapper.xml使用一个namespace,但是【namespace+ID】必须是唯一的,不然动态代理找sql的时候找到两个,就歧义了。如果一个 [mapper接口+方法名]找不到namespace中对应的sql,那么你调用这个mapper接口的这个找不到sql的方法将会报错,但是不会影响项目的其他方法,就是你项目还是能跑起来的,只是执行这个mapper的方法会报错而已。
  2. 要是【namespace+ID】能找到多个sql,那么启动都启动不了。项目启动的时候应该会先把mapper.xml能扫描到的都加载到内存,但是如果【namespace+ID】不是唯一的,扫描到内存的时候就报错了,所以项目会跑不起来。但是,如果只是有一个接口的某个方法找不到【namespace+ID】对应的sql,那么项目还是能启动起来的,只是执行这个mapper接口的这个找不到sql的方法的时候会报错而已。

例子:(后面的例子中,配置的代码我就省略了,和前面一样的。除非有变动。)

package com.yimeng.domain;

import lombok.Data;

@Data
public class User {
    private int id;
    private String userName;
    private String password;
}
package com.yimeng.mapper;

import com.yimeng.domain.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

//@Mapper// 注册mapper接口的bean的方式3:在对应的接口上添加@Mapper注解
public interface UserMapper {
    public List<User> findAll();

    public User findById(Integer id);

    public User selectById(Integer id);
}
package com.yimeng.service;

import com.yimeng.domain.User;
import java.util.List;

public interface UserService {
    public List<User> findAll();
}
package com.yimeng.service.impl;

import com.yimeng.domain.User;
import com.yimeng.mapper.UserMapper;
import com.yimeng.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {
    @Resource
    private UserMapper userMapper;

    @Override
    public List<User> findAll() {
        return userMapper.findAll();
    }
}

ABC.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="com.yimeng.mapper.UserMapper">

<!--    &lt;!&ndash; 跑都跑不起来。因为是在扫描mapper.xml的时候就错误了,扫描是在项目启动的时候进行的,【命名空间标识 + 语句块 ID】必须是唯一的。 &ndash;&gt;-->
    <!--查询操作-->
<!--    <select id="findAll" resultType="user">-->
<!--        select * from user-->
<!--    </select>-->

    <!-- 可以。证明不同的xml可以使用一个namespace。 -->
    <select id="findById" resultType="com.yimeng.domain.User" parameterType="int">
        select * from user where id = #{id}
    </select>
</mapper>

UserMapper.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="com.yimeng.mapper.UserMapper">

    <!--查询操作-->
    <select id="findAll" resultType="user">
        select * from user
    </select>
</mapper>
package com.yimeng;

import com.yimeng.domain.User;
import com.yimeng.mapper.UserMapper;
import com.yimeng.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;

@SpringBootTest
public class SpringbootMybatisApplicationTests {
    @Resource
    private UserService userService;

    @Test
    void findAll() {
        List<User> userList = userService.findAll();
        System.out.println("查询到的数据:"+userList);
    }

    @Resource
    private UserMapper userMapper;
    @Test
    void findUserById() {
        User user = userMapper.findById(1);
        System.out.println("查询到的数据:"+user);
    }

    @Test
    void selectUserById() {
        User user = userMapper.findById(1);
        System.out.println("查询到的数据:"+user);
        user = userMapper.selectById(1);
        System.out.println("查询到的数据:"+user);// 项目启动的时候不报错,但是执行这个语句会报错。因为看到执行userMapper.findById(1)是成功的。
    }
}

执行结果:

image-20250110225548258

image-20250110225605884

image-20250110225635050

SQL映射文件中的顶级元素

SQL 映射文件中只有很少的几个顶级元素,分别是下面几个:

  1. select : 用于查询,支持传参,返回指定结果集;
  2. insert : 用于新增,支持传参,返回指定结果集;
  3. update : 用于更新,支持传参,返回指定结果集;
  4. delete : 用于删除,支持传参,返回指定结果集;
  5. sql : 被其它语句引用的 可复用 语句块;
  6. cache : 当前命名空间缓存配置;
  7. cache-ref : 引用其它命名空间的缓存配置;
  8. parameterMap : 老式风格的参数映射。此元素已被废弃,并可能在将来被移除;
  9. resultMap : 结果集映射,是最复杂也是最强大的元素;

其中,增删改查操作拼接 SQL 时可能会使用到的 动态SQL( 即if、where、foreach啥的)。封装结果集时可能会使用到 复杂映射 (1对1 ,1对多,多对多啥的),这些内容下面会讲。

img

对标签顺序有要求吗?答:没有要求。

上面的9个顶级元素可以为下面几类:

img

其中顶一元素 parameterMap 已建议弃用了 。

无论你有多么复杂的 SQL 操作,最根本的思路都逃不出以上 4 部分。下面就围绕这四个部分来讲讲。

1、insert、delete、update、select、sql标签

select

select 查询语句,几乎是我们最高频的使用元素,所以 MyBatis 在查询和结果映射做了相当多的改进。select 元素允许你指定很多属性来配置每条语句的行为细节。

基本select属性
id、parameterType、resultType属性

例子:

package com.yimeng.config;

import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
//@MapperScan("com.yimeng.**.mapper")// 注册mapper接口的bean的方式1:扫描mapper接口的包注册bean
public class MybatisConfig {

    // 注册mapper接口的bean的方式2:注册MapperScannerConfigurer,扫描mapper接口的包注册bean
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("com.yimeng.**.mapper");
        return mapperScannerConfigurer;
    }
}
package com.yimeng.domain;

import lombok.Data;

//@Data
public class User {
    private int id;
    private String userName;
    private String password;
    private String nick_name;
    // 不开驼峰命名nick_name只能映射到nick_name中去。开驼峰命名nick_name会映射到nickName中去,不会映射到nick_name去。
    private String nickName;

    public User() {
        System.out.println("使用了无参构造方法。");
    }

    public User(int id, String userName, String password, String nick_name, String nickName) {
        System.out.println("使用了有参构造方法。");
        this.id = id;
        this.userName = userName;
        this.password = password;
        this.nick_name = nick_name;
        this.nickName = nickName;
    }

    public int getId() {
        return id;
    }

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

    public String getUserName() {
        System.out.println("使用了getUserName方法。");
        return userName;
    }

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

//    public String getPassword() {
//        System.out.println("使用了getPassword方法。");
//        return password;
//    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getNick_name() {
        System.out.println("使用了getNick_name方法。");
        return nick_name;
    }

    public void setNick_name(String nick_name) {
        System.out.println("使用了setNick_name方法。");
        this.nick_name = nick_name;
    }

    public String getNickName() {
        System.out.println("使用了getNickName方法。");
        return nickName;
    }

    public void setNickName(String nickName) {
        System.out.println("使用了setNickName方法。");
        this.nickName = nickName;
    }
}
package com.yimeng.mapper;

import com.yimeng.domain.User;
import org.apache.ibatis.annotations.MapKey;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.Map;

//@Mapper// 注册mapper接口的bean的方式3:在对应的接口上添加@Mapper注解
public interface UserMapper {
    public List<User> findAll(String username, String password);

    // 如果你装了mybatisX插件,那么idea会提示你返回值是Map的必须要写一个@MapKey("主键")注解。但是没有写也可以执行的,执行不会报错。
    @MapKey("id")
    public Map<String,Object> findOne(Integer id);
}
package com.yimeng;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringbootMybatisLabelApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootMybatisLabelApplication.class, args);
    }
}
<?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="com.yimeng.mapper.UserMapper">

    <!--查询操作。方法返回值是集合,resultType写的是集合的泛型。可以写别名,可以是内置别名或者自定义别名。resultType 与resultMap 不能并用。-->
    <!--parameterType可以省略。当方法参数是多个的时候,parameterType属性必须省略。-->
    <select id="findAll" resultType="user">
        select * from user where username like concat( '%' , #{username}, '%') and password = #{param2}
    </select>
    <!--也可以写一个Map类型-->
    <select id="findOne" resultType="java.util.Map">
        select * from user where id = #{id}
    </select>
</mapper>
<?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>
    <!-- 日志实现 -->
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
        <!-- 驼峰命名 -->
        <setting name="mapUnderscoreToCamelCase" value="true" />
    </settings>
</configuration>
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatis-label
    username: root
    password: 815924
    type: com.alibaba.druid.pool.DruidDataSource

mybatis:
  # 加载全局的配置文件(mapper的xml可以通过yaml指定加载,但是mapper接口得在配置类中使用@MapperScan("com.yimeng.**.mapper")或者注册MapperScannerConfigurer或者在接口上面使用@Mapper注解)
  config-location: classpath:mybatis/mybatis-config.xml
  # 加载映射文件
  mapper-locations: classpath:mapper/*.xml
  # 指定别名包
  type-aliases-package: com.yimeng.domain
package com.yimeng;

import com.yimeng.domain.User;
import com.yimeng.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

@SpringBootTest
public class SpringbootMybatisApplicationTests {
    @Resource
    private UserMapper userMapper;

    @Test
    void findAll() {
        List<User> userList = userMapper.findAll("三","123456");
        System.out.println("查询到的数据:"+userList);
    }
    @Test
    void findOne() {
        Map<String, Object> user = userMapper.findOne(1);
        System.out.println("查询到的数据:"+user);
    }
}

结果:

image-20250114233352957

image-20250114232330882

image-20250114232400154

属性:

  • id:唯一的标识符。在当前命名空间下,sql的 id 值是唯一的。总之「 命名空间标识 + 语句块 ID 」一定是唯一的。如果存在相同的 “ 命名空间 + 语句id ” 组合,Mybatis 将在项目启动的时候就抛出报错。
  • parameterType:传给此语句的参数的全路径名或别名。例:com.test.poso.User或user。parameterType也可以省略,MyBatis 可以根据上下文自动推断出参数的类型,从而减少配置的冗余性。MyBatis 能够省略 parameterType 参数的能力与 类型处理器(TypeHandler) 有密切关系。正是因为 MyBatis 内置了强大的类型处理器机制,才能够在不显式指定 parameterType 的情况下,自动推断参数类型并完成类型转换。它会通过类型处理器把你java的参数转为jdbc的类型的值,然后放到sql中的占位符去。注意:如果这个方法参数有多个,那么我们就不能写parameterType 属性了。mybatis内置了一些类的别名,我们可以直接用那些别名,比如,map接口的方法形参是Integer类,对应的select标签的parameterType就可以使用int,因为内置int是Integer的别名。如果是mybatis内置别名中没有的映射关系,那么你想要使用别名的话,你可以自定义别名,这个之前讲过,这里不讲。
  • resultType:sql语句返回值类型全限定名或别名。注意,如果是集合,那么这里填写的是集合的泛型,而不是集合本身。注意:resultType 与resultMap 不能并用。resultType属性,要完全一样才能映射(不开驼峰命名nick_name只能映射到nick_name中去。开驼峰命名nick_name会映射到nickName中去,不会映射到nick_name去)。结果集映射到实体类是用实体类的set方法去进行绑定值的。但是注意哈,sql中的#{password},不是调用实体类的getPassword去获取值的,我试了,就算实体类中没有getPassword方法,#{password}一样可以拿到实例的password值,可能是通过反射吧。resultType是一个实体类,那么mybatis会使用了无参构造方法去创建这个对象,然后用set方法进行赋值。指定返回一个 Map类型的对象,mybatis 会把查询出来的数据表记录对应的 ’ 字段列名 - 字段值 ',自动映射为 map 集合的 key - value 。如果这里写的是某个实体类,那么查询的字段值将会封装到实体类对应的属性中去。resultType是简单映射,需要查询结果的列名与java的属性名完全一致才能完成映射,如果开启了驼峰命名就是查询的列名变为驼峰命名,再与java属性名进行比对,完全一样的才能完成值的绑定,resultType只依赖默认类型处理器,不能自己专门指定某个字段和java属性的映射使用特定的类型处理器来完成。而resultMap则需要开发者显式定义映射规则,比较适用于复杂的映射场景。resultType不能完成一对多或者多对多的实体类关系映射,只能是查询的结果和实体类的属性名直接匹配,然后绑定值,不能把值绑定到一个实体类中的另一个实体类属性中去。

如果方法的参数有多个,那么Mybatis (ParameterType) 应该如何传递多个不同类型的参数呢?

1、方法一:不需要写parameterType参数

public List getXXXBeanList(String xxId, String xxCode);

<select id="getXXXBeanList" resultType="XXBean">
select t.* from tableName where id = #{param1} and name = #{xxCode}
</select>

由于是多参数那么就不能使用parameterType,sql中可以使用#{paramXX}是第几个就用第几个的索引,索引从1开始。或者使用{方法形参变量名}、{arg0}获取参数值。(不同版本的mybatis规则不太一样,有效版本可以使用{方法形参变量名}获取,但是有些版本不行,有些版本可以使用{arg0}获取参数值,但是有些mybatis版本又不行)

2、方法二:基于注解(最简单)

public List getXXXBeanList(@Param(“id”)String id, @Param(“code”)String code);

<select id="getXXXBeanList" resultType="XXBean">
select t.* from tableName where id = #{id} and name = #{code}
</select>

由于是多参数那么就不能使用parameterType, 这里用@Param来指定哪一个

3、方法三:Map封装

public List getXXXBeanList(HashMap map);

<select id="getXXXBeanList" parameterType="hashmap" resultType="XXBean">
select 字段... from XXX where id=#{xxId} code = #{xxCode}
</select>

其中hashmap是mybatis自己配置好的直接使用就行。map中key的名字是那个就在#{}使用那个,map如何封装就不用了我说了吧。

4、方法四:Java Bean传参法(推荐)

public User selectUser(User user);

<select id="selectUser" parameterType="com.test.User" resultMap="UserResultMap">
    select * from user
    where user_name = #{userName} and user_age= #{age}
</select>

5、方法五:List封装

public List getXXXBeanList(List list);

<select id="getXXXBeanList" resultType="XXBean">
  select 字段... from XXX where id in
  <foreach item="item" index="index" collection="list" open="(" separator=","close=")">
    #{item}
  </foreach>
</select>
进阶select属性
mybatis缓存

可以看:https://blog.csdn.net/m0_62943934/article/details/140607963

我们先了解一下缓存。

  1. 一级缓存:

    • 一级缓存是SqlSession级别的缓存。

    • 作用域是在一个 SqlSession 内。

    • 一级缓存是 MyBatis 的默认开启的,且不能被关闭的,mybatis一级缓存默认是session级别的,即SqlSession的生命周期是一个会话(会话的生命周期:从开启数据库连接,到关闭数据库连接),虽然一级缓存不能被关闭,但是可以调整一级缓存的作用域,比如在mybatis的核心配置文件中加上,就能把作用域改为语句级别缓存,如果把一级缓存的作用域调整为语句级别,就是一个sql语句一个SqlSession了,即,一级缓存只对当前执行的语句生效,执行完这一句sql后SqlSession就关闭了,就清空一级缓存了,所以如果把缓存调整为statement级别,那么效果就类似于关闭了一级缓存。

    • Mybatis底层是一个HashMap,是在内存的,一级缓存也叫本地缓存。

    • 一级缓存不支持使用第三方缓存替代,二级缓存支持使用第三方缓存。

    • 一级缓存不需要实体类要满足可序列化。二级缓存一般需要满足实体类可序列化(二级缓存配置mapper.xml的时候,设置为只读,实体类就可以不用可序列化。但是如果使用redis作为第三方缓存,那么还是需要可序列化的)。

    • 一级缓存没有刷新策略。二级缓存有缓存刷新策略。

    • MyBatis 会自动将查询结果缓存到 SqlSession 中,以便同一 SqlSession 内的多次相同查询可以直接从缓存中读取,而不再发出 SQL 请求。

    • 不同SqlSession之间是隔离的,即,如果一个SqlSession执行了增删改操作,只会清空这个执行了增删改的SqlSession,不会清空其他的SqlSession。

  2. 一级缓存的更新机制:

    一级缓存会在以下情况下进行清空或更新:

    • 同一 SqlSession 中的增删改操作:当执行 INSERT、UPDATE 或 DELETE 操作后,MyBatis 会将当前 SqlSession 的一级缓存清空,以确保缓存数据与数据库数据的一致性。(清空)(就算没有提交,也会清空)
    • rollback回滚会清空缓存。(清空)
    • commit提交也会清空缓存。(清空)
    • 关闭session会清空缓存。(清空)
    • 手动清空缓存:可以调用 sqlSession.clearCache() 方法手动清除一级缓存。(清空)
  3. spring集成mybatis时一级缓存怎么体现

    在spring集成mybatis的项目中,在一个没有加事务的方法里,你两次执行一样的查询,并不会走一级缓存。但是这个方法加了事务,在事务方法中,执行两次一样的查询,第二次查询会走一级缓存。这是为什么?

    首先,我们要先解决一个问题:我们知道,spring集成mybatis后,我们没有手动获取SqlSession了,我们是注入mapper接口,然后调用mapper接口中的方法就行了。那么他的SqlSession是什么时候获取,又是什么时候关闭的呢?

    在查阅了资料后得知,在 Spring 集成mybatis的情况下,会通过 SqlSessionTemplate 来管理 SqlSession,开发者不需要手动处理 SqlSession 的打开和关闭,Spring 会自动管理。那么在spring集成mybatis的环境下,SqlSession是什么时候创建,又是什么时候关闭的呢?这个要分情况:

    • spring集成mybatis时,在没有事务的情况下,每次请求数据库的时候都会重新创建一个SqlSession(没有开启事务不会把sqlsession存入当前线程),执行完了就关闭Sqlsession。用完就关闭了,所以,你会看到,没有开启事务的情况下,你两次执行一样的查询,并不会走一级缓存。因为第一次查询结束后就关闭了SqlSession,第二次查询又创建新的SqlSession,所以他们使用的不是一个SqlSession,所以无法命中一级缓存。
    • spring集成mybatis时,在事务的情况下,执行数据库方法,会去threadLocal中获取SqlSession,如果获取不到,那么会创建一个新的SqlSession并放到threadLocal中,在事务提交或者回滚的时候会清空SqlSession,并释放资源。因为会先去threadLocal中获取SqlSession,SqlSession会在事务结束时才进行清空,所以这样就可以做到一个事务下,不同的访问数据库方法使用一个SqlSession了。所以,可以做到,在一个事务内,执行两次一样的查询,第二次查询能命中缓存了。绑定到threadLocal中还有一个好处,就是避免了并发问题,因为threadLocal是依赖于线程的,A线程无法访问到B线程中的threadLocal,然后sqlsession是在thread内的,所以不同线程获取的sqlsession就一定不是一个对象了,这样就不会出现并发问题了。tomcat每次收到一个请求,都会开一个线程去执行的哈,这个是tomcat的机制,tomcat也有自己的一个并发量控制,最大不能超过多少个线程。数据库也是可以并发的,比如多个请求同时去访问数据,是可以做到的,只是可能会出现数据库锁的一些问题,需要注意就是了。(ThreadLocal是线程中的一个容器,每一个线程都有可以有自己的ThreadLocal,线程之间的ThreadLocal是相互独立,他的结构其实就是一个Map<K,V>,一个线程的ThreadLocal初始值为null,我们可以自己声明一个ThreadLocal对象,然后把一些对象或者数据,放到ThreadLocal中,然后这样其他要执行的语句想要获取这些值的时候,就可以通过拿到语句执行所在的线程的ThreadLocal中的值。注意,使用完ThreadLocal最好清理一下ThreadLocal中的数据,因为ThreadLocal是依赖与线程的,如果使用了线程池,那么线程可能会被再使用,所以这个ThreadLocal要是使用完不清理,那么如果再复用到这个线程的时候,并且去取ThreadLocal中的数据的时候,可能会有脏数据(上一次线程结束还保留的数据)。这里不具体讲ThreadLocal的使用哈。)

    当然哈,上面说的都是在spring集成mybatis的情况下哈。spring集成mybatis的情况下会使用SqlSessionTemplate 来管理 SqlSession,管理的效果就是上面这个内容了。如果我们自己手动通过SqlSession sqlSession = sqlSessionFactory.openSession();获取SqlSession,获取的是DefaultSqlSession,我们获取后,要手动关闭,并且要手动解决线程安全问题(虽然前面的案例中我们手动获取的SqlSession都没有去解决线程安全问题哈,但是如果真要手动创建SqlSession的话,需要解决线程安全问题哈),SqlSessionTemplate 其实就是一个封装的类,这个封装的类底层也是用DefaultSqlSession的,只是,他进行了封装,他把线程安全和手动获取释放sqlsession的操作都给我们做好了,spring集成mybatis底层就是用的SqlSessionTemplate 类,而不是直接使用DefaultSqlSession的。更多内容可以看https://zhuanlan.zhihu.com/p/100533979/、https://www.cnblogs.com/Higurashi-kagome/p/18645930等文章。

  4. 一级缓存的生效条件:

    一级缓存只有在以下条件下才会生效:

    • 同一个 SqlSession 实例:只有在同一个 SqlSession 中,一级缓存才会生效。如果创建了新的 SqlSession,则该会话没有之前的缓存数据。(其实就是说,缓存有一定的作用域,一级缓存的作用域是一个Sqlsession内,所以出了这个作用域就无效了。)
    • mapper.xml的nameSpace+StatementId、查询参数、分页参数、传递给JDBC的SQL语句、执行数据库环境(你执行sql使用的mybatis中的数据库环境,在mybatis核心配置文件中的那个environments标签配置的)必须都相同:相当于这几个参数的组合是本地缓存HashMap中的键,而查询并封装后的对象是HashMap的值。因此,会出现一种情况,即第一次查询返回了一个对象,这个对象是查询数据库,然后mybatis封装返回的,第二次查询命中了这个一级缓存,那么,这两个查询拿到的对象会是一个对象,即,这两次查询返回的是一个引用,指向了一个内存地址。所以只要改变那个内存中的对象的数据,第一次和第二次返回的对象都会改变。
    • 没有执行清空一级缓存操作:只要执行了就清空了,就要重新查询数据库了,增删改操作、事务提交、事务回滚、关闭sqlsession、手动执行sqlSession.clearCache()方法都会清空一级缓存。
  5. flushCache使一级缓存失效:

    配置flushCache="true"就行。这样配置后,执行这个语句,会清除这个语句所在的SqlSession缓存。不会清空其他SqlSession的缓存。并且这个查询也不会走缓存。就算你设置了useCache="true"也不行。

    <select id="findUser" resultType="user" flushCache="true">
        select * from user where username like concat( '%' , #{username}, '%') and password = #{param2}
    </select>
    

    useCache="false"对一级缓存无效。设置为false后,依然会走一级缓存。useCache属性只会对二级缓存有影响,所以在一级缓存中,你加了useCache="false"也没有用,不管你写的true还是false都会被忽略的。

  6. 二级缓存:

    • 二级缓存是作用于 Mapper 级别的缓存机制(Mapper级别就是,作用域为namespace)。二级缓存的作用域是一个namespace内,他可以跨 SqlSession 共享,并且可以跨事务共享。
    • 二级缓存默认也是关闭的(全局配置文件中的cacheEnabled 的默认值是 true,默认是开启的,但是还需要在要开启二级缓存的mapper.xml中使用才算开启)。标签的作用就是让所在的xml文件中的所有select标签都走二级缓存(当然,如果你在这个xml中的某个select标签中写了useCache属性为false,那这个select执行不会走二级缓存。其他select标签还是会走二级缓存的哈)。注意:标签是把标签所在的xml下的select标签都设置为走二级缓存哈,不是把标签所在的namespace下的所有select标签都设置为走二级缓存。我测试过,一个UserMapper接口,写两个xml,这两个xml的namespace都写UserMapper的全限定名,我其中一个xml中写了标签,另一个没有写标签,结果写了标签的xml写的查询可以走二级缓存,但是没有写标签的xml下的select不会走二级缓存。并且我也试了在两个xml都写标签,结果报错了,应该是一个namespace只能写一个。当然像这样一个mapper接口写两个mapper.xml的做法是不规范的,如果你按照规范的写法去做,都遇不到上面这个问题,所以不用太深究。
    • 默认情况下,我们开启了二级缓存的全局配置(默认开启),也在mapper.xml中写了标签,但是要把查询数据写入到二级缓存的时候发现实体类不能序列化,那么会报错。所以要想使用二级缓存需要实体类实现Serializable接口(应该是接受mybatis返回结果的实体类需要Serializable接口)。mybatis写入二级缓存的时机是关闭连接和事务提交的时候。
    • 如果没有在mapper.xml中使用,那么这个mapper.xml中的select不会走二级缓存。
    • 如果mapper.xml中使用的指定为只读,比如。那么可以实体类可以不用支持序列化。但是如果你使用了redis作为第三方缓存,那么readOnly="true"对redis无效,你实体类还是需要支持序列化才行。所以最好让所有的要接收mybatis返回值的实体类都是可序列化的。
    • 默认情况下二级缓存是用序列化和反序列化的手法来保存数据的,而一级缓存是通过内存中保存对象的引用来实现的。如果,在mapper.xml中写的是,那么这个mapper.xml的select也是以引用的方式来保存数据到二级缓存的。所以,如果使用了,那么第一次查询到结果对象会把引用放到二级缓存中,下次查询,命中这个缓存,二级缓存会直接返回引用。当然哈,这个mapper.xml写了,不会影响其他的mapper.xml哈。注意,序列化和反序列化保存二级缓存对象的做法其实是比保存引用慢的,因为序列化和反序列化也是需要时间的。
    • 如果设置为,但是你把查询到的结果修改了,会报错吗?其实也不会,不会报错的。你写了只是你对mybatis说,你不会去修改二级缓存查询到的结果而已。但是你实际是否去修改,mybatis不管你。就是说,如果你向mybatis说你不会去修改二级缓存查询的结果,那么他就会放心让你不走序列化。
    • 二级缓存也叫全局缓存。
    • 二级缓存在mapper.xml中写了,那么这个mapper.xml的所有select的方法都开启了二级缓存。如果你想让某个select不走二级缓存,你可以把这个select标签的useCache属性设置为false,那么该方法就不会使用二级缓存。useCache属性只会影响这一个方法,对其他方法没有任何影响。
    • 如果select中配置了flushCache=“true”,那么执行这个sql的时候,就会清空这个select所在的namespace中所有的二级缓存。并且这个select方法也不会走二级缓存。select的useCache默认是true的。在flushCache=“true” useCache="true"或者只写了flushCache="true"的情况下,这个select不会走二级缓存,就算二级缓存中能找到它的key也不会走,执行查询后会清空二级缓存,并且把这个当前这个select查询到的结果放入缓存。如果select是在写了flushCache=“true” useCache="false"的情况下,那么这个select查询不会走二级缓存,也会清空二级缓存,但是不同点是,不会把查询结果放到二级缓存中去了。所以建议,如果写flushCache="true"也把useCache="false"写上,因为你写了flushCache="true就不会走二级缓存,那么你还把查询结果放到二级缓存中去干嘛呢,浪费性能。
    • 在不同namespace之间的二级缓存是隔离的,不会相互影响,即,一个namespace执行了增删改操作,清空了缓存是只清空对应的namespace缓存,不会影响其他的namespace缓存。
    • 二级缓存是多线程共享的。A线程也能会访问到B线程保存的二级缓存。但是一级缓存是线程私有的,不会共享的。
    • 只有会话关闭或提交后才会写入二级缓存。在一级缓存Session关闭的时候,才会开始创建二级缓存。提交也是会创建二级缓存。rollback和clearCache不会对二级缓存有任何影响。特别注意rollback无法产生二级缓存。
    • 二级缓存开启的情况下,执行查询流程是先查二级缓存,再查一级缓存,最后再查数据库。

    img

  7. 二级缓存的启用和配置:

    • 开启全局配置:在 mybatis-config.xml 中,需设置 。或者保持默认也行,默认就是开启的。

    • 单个 Mapper 启用缓存:在 Mapper 映射文件(*Mapper.xml)中,通过 标签开启二级缓存。例如:

      <mapper namespace="com.example.mapper.UserMapper">
          <cache />
      </mapper>
      
    • 缓存策略配置:在标签中可以指定一些属性,比如可以通过 flushInterval 设置刷新间隔,可以通过size 设置缓存大小,可以通过readOnly 设置缓存是否为只读(标签是写在mapper.xml中的)。如下所示:

      <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
      
      • eviction:缓存的淘汰策略,默认为 LRU(最近最少使用)。目前MyBatis提供以下策略。

        (1) LRU,最近最少使用的,一处最长时间不用的对象

        (2) FIFO,先进先出,按对象进入缓存的顺序来移除他们

        (3) SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象

        (4) WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。这里采用的是LRU,移除最长时间不用的对形象

      • flushInterval:刷新间隔,单位为毫秒。默认不刷新。如果你配置了,那么这么多毫秒就会清空对应namespace的二级缓存。每当有数据被存入二级缓存时,flushInterval 的计时器会重置为当前时间。就是说,是从最后一次操作这个namespace二级缓存的时候开始计时的,如果在计时到达之前又有操作这个二级缓存的操作,那么会重新计时。

      • size:引用数目,是一个正整数,他代表二级缓存最多可以存储多少个对象,但是要注意,不宜设置过大,因为设置过大会导致内存溢出。默认值是 1024。当缓存对象的个数到达size个数时,会使用eviction指定的缓存淘汰算法来清理部分二级缓存对象,而不是全部清空namespace对应的二级缓存。

      • readOnly:是否只读,默认值是 false。

      • type:指定自定义缓存的全类名(这个类需要实现Cache接口),你可以用type指定第三方缓存作为mybatis的二级缓存。当然你也可以不使用第三方缓存的,不指定就是使用mybatis默认的二级缓存。

  8. 二级缓存的生效条件:

    二级缓存的命中的key也是mapper.xml的nameSpace+StatementId、查询参数、分页参数、传递给JDBC的SQL语句、执行数据库环境。

  9. 二级缓存的更新机制:

    • 当执行 INSERT、UPDATE 或 DELETE 操作后并提交事务,MyBatis 会自动清空对应命名空间(Mapper 级别)的二级缓存,以确保数据一致性。注意,如果执行增删改操作,但是没有提交事务时,那么不会清空二级缓存的,因为可能回滚,所以mybatis没有执行清空二级缓存的操作。但是执行了增删改操作,就算没有提交也会清空一级缓存的。(虽然执行增删改后,没有提交前,不会清空二级缓存,但是看到结果是,在这个执行增删改后到提交前的期间内,如果查询命中了二级缓存,可拿到的结果是查询数据库的结果,而不是取二级缓存中的结果,可能这里mybatis底层做了优化吧。它优化的结果可能是,在执行了增删改操作之后提交之前,那个namespace的二级缓存虽然没有被清空,但是是被设置为了不可用的状态,所以,虽然在执行增删改后到提交前有查询命中这个不可用的二级缓存,但是实际还是会走查询数据库。为什么不清空缓存呢?可能是因为如果执行了增删改操作后,进行了回滚,那么清空就没有必要了,所以就暂时没有清空了。如果回滚了,那么二级缓存又会被重新设置为可用状态的。执行增删改操作后,提交后会清空二级缓存的。)
    • 如果在 标签中配置了 flushInterval 属性,MyBatis 会按照指定的时间间隔刷新flushInterval 所在这个namespace的二级缓存。
    • 执行了带有 useCache=“false” 的查询,会清空 useCache="false"所在的namespace二级缓存。
    • 在一级缓存Session关闭的时候,才会开始创建二级缓存。提交也是会创建二级缓存。rollback和clearCache不会清空二级缓存的。**特别注意rollback无法产生二级缓存。只有Session关闭的时候和提交会创建二级缓存。**特别注意:执行sqlSession.clearCache只能清空一级缓存,对二级缓存是没有效果的哈。
    • 二级缓存到达size也会清理二级缓存的。MyBatis 会根据缓存的替换策略来决定哪些缓存数据需要被淘汰。默认情况下,MyBatis 使用的是 LRU (Least Recently Used) 缓存替换策略。这意味着,当缓存达到上限时,最近最少使用的缓存数据会被首先淘汰。这个是清空部分二级缓存,不是清空全部的二级缓存。
    • 当应用关闭会清空二级缓存。但是如果使用第三方缓存作为二级缓存,应用关闭不会自动清空第三方的二级缓存。
  10. 二级缓存取出的对象:

    前面说过,一级缓存里面存储的是对象的引用(内存地址),当对象内容修改了,一级缓存里面存储的那个对象内容也会随之修改,而二级缓存里面存储的是对象序列化之后的值,换句话来说就是类似与拷贝了一份新的数据到二级缓存;因此修改原有对象不会影响,之后从二级缓存里面取出的对象;而这样带来的弊端就是每次session关闭的时候都需要将数据序列化一份,这不仅从时间角度上效率低了,从空间角度上效率也会变低,因为每次保存结果到二级缓存的时候都会序列化;

    如果我们对数据的修改并不敏感,或者确定不会对数据进行修改(你设置为了readOnly="true"你还是可以去修改对象的哈,这里你去修改了只是你自己违背了你和mybatis说不会去修改的承诺,你说不会去修改的,所以二级缓存给你保存了引用,在你确实没有修改的情况下是没有问题的,但是如果你去修改了,那么就是你的问题,也不是mybatis的问题,是你自己告诉mybatis你不去修改的嘛),那么我们可以将二级缓存的readOnly设置为true;此时二级缓存里面存储的是对象的引用,并不会采用序列化技术,此时的对象也不需要实现序列化接口了;

    <cache readOnly="true"/>
    

    反序列化的对象是一个新对象。所以没有开启 readOnly=“true” 的情况下,命中了二级缓存,就是从二级缓存中反序列化拿到一个新对象,反序列化的这个对象会和第一次存入二级缓存时的那个对象(序列化到二级缓存的那个对象)不是一个对象。

    但是如果开启了readOnly="true"那么,二级缓存也和一级缓存一样,保存的是引用到二级缓存中,而不是使用序列化、反序列化的手法来保存的。

  11. 一二级缓存作用域对比:

    resize,m_fixed,w_1184

  12. 开启二级缓存后,用户查询时,会先去二级缓存中找,找不到在去一级缓存中找;

    在这里插入图片描述

    img

  13. cache-ref:

    cache-ref 只对二级缓存有效,没有使用二级缓存时,cache-ref那么这东西没有意义。cache-ref 标签需要指定一个其他 Mapper 的namespace,并且这namespace必须是已经配置了二级缓存的,不然会报错 。

    原因是:首先mybatis解析每个XML时,会创建XMLMapperBuilder调用cacheElement方法解析标签,如果有标签,则会被XMLMapperBuilder的全局变量builderAssistant解析成Cache接口对象(该对象是使用了构建者、装饰者模式,具体读者自行看源码)。然后configuration(mybatis最重要的全局配置属性)的caches(Map类型)会添加key(key是xml的namespace)。所有的XXMapper.xml的cache解析完了后,接下来会解析有引用的XML,同理,mybatis解析XML时会创建XMLMapperBuilder,解析标签。解析时,会去configuration.caches中找对应的缓存,如果在解析时发现configuration.caches没有中写的这个key(key是xml的namespace),就会报错。就算你把mybatis全局配置文件中把二级缓存关闭,,指向的namespace中要是没有,那也会报错。

    使用cache-ref的效果是:让当前命名空间使用另一个命名空间Cache对象作为当前命名空间的Cache对象,就是共享二级缓存。就是相当于借用缓存空间,让两个xml中的select使用同一个二级缓存空间。

    写法:。

    什么时候需要使用cache-ref:当mybatis的当前命名空间存在DML的事务提交时,会使当前命名空间里的缓存失效,这样在查询时,会直接从数据库拿到数据,并再次缓存。但是如果是多表连接查询,如tableA join tableB,A表的DML操作在A的nameSpace,联查的操作也在A的namespace进行的,B表的DML操作在B的nameSpace,当你先进行了tableA join tableB操作,并把数据保存到了二级缓存中,然后B表在进行了数据修改,但是这个B进行的修改不会使A表缓存失效,所以当你再使用tableA join tableB会直接从缓存中获取数据,因为此时缓存没有刷新,而且B表的数据也有变化,那么此时读取的就是脏数据。这种情况就需要在B的nameSpace里使用,保证更新B时,A的缓存也失效。但是使用了后,这两个namespace进行增删改的操作都会使二级缓存失效,这样二级缓存清空的频率就更高了,所以在多表联查的需求下,不建议使用二级缓存。

  14. 你经常会看到二级缓存不建议使用的说法,原因是:

    一、脏数据:因为二级缓存是基于 namespace 的,比如在 StudentMapper 中存在一条查询 SQL,它关联查询了学生证件信息,并且开启了二级缓存,而学生证件写在另外一个namespace中,那么就可能出现脏数据。比如,你使用StudentMapper 多表联查查询了数据库,并保存到二级缓存中,然后,你使用学生证件那个namespace中的更新语句更新了数据库数据,但是StudentMapper的namespace的二级缓存不会知道这个事情,所以缓存没有失效,这时StudentMapper再进行查询,就直接拿到了缓存中的数据,但是数据库中的数据现在已经不一样的,所以在 StudentMapper 中的数据就是脏数据了;你可以解决的方法是使用cache-ref来解决,但是使用cache-ref来解决就会导致缓存的粒度变粗了,多个namespace下的增删改操作都会对二级缓存使用造成影响,二级缓存会频繁被清空,二级缓存的作用就不大了,因为清空后,命中率就低了。

    二、易清空而导致的命中率低:因为只要执行增删改操作,那么这个namespace中的二级缓存数据就会全部清空。如果经常清空,那么缓存命中率可能会很低。在经常清空的情况下,你使用二级缓存反而会降低应用程序的性能,因为你的查询比不使用二级缓存多了一个序列化,并且你二级缓存命中率不高。

  15. 注意:

    在同一个xxxMapper.xml文件同时配置和,这时当前命名空间是独享缓存还是与引用的缓存共享?

    这里比较复杂,不用研究了。总之,最好不要在一个mapper.xml中同时配置<cache/>和<cache-ref/>标签,这样写不规范。

  16. 配置使用第三方缓存(自定义缓存):

    Mybatis自身的二级缓存其实是很简陋的,其顶层接口为Cache,查看其具体实现,底层其实就是个Map数据结构而已,因此mybatis给你提供了集成第三方缓存接口的相关接口,你可以使用第三方缓存来作为二级缓存,如 Ehcache、Redis、Hazelcast 等。

    比如怎么使用redis作为第三方缓存呢?

    要使用第三方缓存作为mybatis的二级缓存,需要实现org.apache.ibatis.cache.Cache接口,并实现相关的逻辑,这样mybatis才能使用你指定的来作为二级缓存进行二级缓存相关的操作。

    redis作为比较主流的第三方缓存,mybatis提供了支持,mybatis提供了一个mybatis-redis的架包,这个架包中有一个RedisCache类,这个类就实现了mybatis的Cache接口。所以我们直接使用这个类就可以做到让mybatis使用redis作为mybatis的二级缓存。

    做法:

    1️⃣:引入依赖

    <dependency>
        <groupId>org.mybatis.caches</groupId>
        <artifactId>mybatis-redis</artifactId>
        <version>1.0.0-beta2</version>
    </dependency>
    

    2️⃣:编写redis的配置文件redis.properties

    # Redis服务器地址
    redis.host=127.0.0.1
    # Redis服务器端口
    redis.port=6379
    # Redis服务器密码(如果没有密码,可以留空)
    redis.password=
    # 连接超时时间(毫秒)
    redis.timeout=5000
    

    3️⃣:在需要开启二级缓存的XXMapper.xml中使用标签指定二级缓存使用的第三方缓存是什么即可。

  17. 使用第三方缓存作为二级缓存,执行flushCache="true"的select也会清空二级缓存。执行增删改操作后不会清空二级缓存,提交才会清空二级缓存。(看到和使用mybatis自带的二级缓存的规则是一样的。唯一不同的就是,程序执行结束,mybatis自带的二级缓存会清空,但是redis不会,所以我执行了测试方法查询并存入二级缓存,即存入redis,第二次使用另一个测试方法,执行一样的查询就看到命中了二级缓存了。但是如果是用自带的二级缓存就不会命中。)

  18. 下面四个对缓存的配置(如清除策略、可读或可读写等),对第三方缓存是否有效。通过测试,MyBatis 官方提供的 mybatis-redis 缓存实现(org.mybatis.caches.redis.RedisCache不支持这些参数。Redis 本身是一个独立的分布式缓存系统,它的缓存淘汰策略、刷新机制和容量限制需要通过 Redis 的配置文件来设置,而不是通过 MyBatis 的缓存参数。你设置readOnly=“true”,如果实体类也实现Serializable接口也是会报错的,因为这个参数也是对第三方缓存无效的,它还是会走序列化到redis,反序列化到java内存的方案的。

    <cache
      eviction="FIFO"
      flushInterval="60000"
      size="512"
      readOnly="true"/>
    
  19. mybatis的二级缓存什么时候清空,什么存入,我们可以通过使用redis作为第三方缓存来看看存入和清空的时机,因为存入和清空的时机和mybatis自带的缓存时一样的。对与mybatis二级缓存的键长什么样,也可以通过redis来直观地看到,只有键一样才能命中二级缓存:

    image-20250123142418451

  20. 注意:前面说的提交其实都是指的是事务提交。

flushCache、useCache、resultMap属性

flushCache:是否清空缓存。默认是false,如果你将他设置为 true ,那么只要被执行,就会导致一级缓存和二级缓存被都清空。这个是对一级缓存也有效的,也会清空一级缓存的。会清空对应的sqlSession的一级缓存,不会影响其他的sqlSession的。对二级缓存只会清空当前语句匹配的namespace二级缓存。

useCache:useCache可以控制当前SQL语句的结果集是否要存入二级缓存;默认情况下为true。useCache只能控制二级缓存,并不会影响一级缓存;useCache需要当前的mapper.xml开启二级缓存功能后才有效果;useCache="false"对一级缓存无效。在mybatis核心配置文件开启二级缓存后,在mapper.xml中加上缓存,会让这个mapper.xml对应的namespace中所有的查询都走二级缓存,如果你不想让其中某个查询语句走二级缓存,那么你可以在那个sql上加上useCache="false"属性,这样就做到精细控制每个查询是否走二级缓存了,不想走二级缓存的加上这个useCache="false"属性,其他的都走二级缓存。

resultMap:对外部 resultMap 的命名引用(外部的resultMap后面会讲,外部的resultMap还是比较复杂的,select中的resultMap属性还是简单的,只是说映射关系看外部的哪个resultMap而已)。结果映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂的映射问题都能迎刃而解,后面一对一、一对多、多对多内容的讲解也和这个属性有关。 resultType 和 resultMap 之间只能同时使用一个。写resultMap的是,那些能自动映射进行绑定的对应关系,你可以写也可以不行,都是可以的(最好自己手动把映射关系指定好,不用他自动映射,因为一部分情况下,他是无法自动映射的,这些情况我也无法全部区分出来,所以最好不用他自动映射,我们都自己写映射关系)。

image-20250123163624049

其他select属性
parameterMap、timeout、fetchSize、statementType、resultSetType、resultSets、databaseId属性
属性 作用 默认值 示例
parameterMap 指定参数映射规则(已过时) parameterMap="userParameterMap"
timeout 用于设置 SQL 执行的超时时间,单位是秒(s),超时将抛出异常 null timeout="10"
fetchSize 设置 JDBC 的 fetchSize,控制每次从数据库获取的记录数。这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。由于性能问题,建议在 sql 做分页处理。 由 JDBC 驱动决定 fetchSize="100"
statementType 告诉 MyBatis 使用哪个 JDBC 的 Statement 工作,取值为 STATEMENT(Statement)、 PREPARED(PreparedStatement)、CALLABLE(CallableStatement) PREPARED statementType="PREPARED"
resultSetType 指定了MyBatis应该如何处理数据库的游标(结果集类型),它影响MyBatis对结果集的滚动和更新行为。常见的值有 FORWARD_ONLYSCROLL_INSENSITIVESCROLL_SENSITIVE FORWARD_ONLY resultSetType="SCROLL_INSENSITIVE"
resultSets 指定存储过程返回的多个结果集的名称。 resultSets="users,orders"
databaseId 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。 databaseId="mysql"

上面内容就讲可能会用到的几个属性,其他都不讲,因为用得少。

  1. timeout:用于设置 SQL 执行的超时时间,单位是秒(s),超时将抛出异常。并且这个值只能是整数,不是是小数。

    • 如果执行时间超过 timeout 设置的值,MyBatis 会抛出异常。

    • 默认值为 null,表示没有超时限制。

    • 增删改也可以使用这个参数

    • 例子:

      <select id="getUserById" resultType="User" timeout="10">
          SELECT * FROM user WHERE id = #{id}
      </select>
      
  2. statementType:指定 SQL 语句的类型。

    • 可选值:
      • STATEMENT:使用 java.sql.Statement 执行 SQL。Statement每次执行sql语句,数据库都要执行sql语句的编译,并且有SQL注入的风险,所以不建议用。就是之前学jdbc的那个Statement。
      • PREPARED:使用 java.sql.PreparedStatement 执行 SQL(默认值)。使用预编译,只要编译一次,然后进行占位符的替代就行了,对于多次执行的效率很高。并且安全性好,可以有效防止Sql注入等问题。这个就是之前学jdbc的那个PreparedStatement。
      • CALLABLE:使用 java.sql.CallableStatement 执行存储过程。当使用存储过程的时候,需要指定statementType的值为CALLABLE。
    • 说明:
      • PREPARED 是推荐的方式,因为它支持预编译和参数化查询,可以提高性能并防止 SQL 注入。
    • STATEMENT不怎么推荐,PREPARED默认就是,接触很多,这个不演示了,默认省略就是,所以这里就CALLABLE我们用得少一些。我们这里就演示一下statementType为CALLABLE的例子。statementType为CALLABLE就是存储过程,存储过程的例子我们和resultSets一起讲。
  3. resultSets:指定存储过程返回的多个结果集的名称。普通查询通常只返回一个结果集,但存储过程可以返回多个结果集。resultSets主要就是用于存储过程(Stored Procedure)的。

    • 说明:
      • 当调用存储过程时,如果存储过程返回多个结果集,可以使用 resultSets 指定每个结果集的名称。
      • 名称之间用逗号分隔。
    • 例子:看存储过程的知识点。
存储过程

mybatis写存储过程的做法如下。(注意,这里只了解一下就行了,因为现在mybatis中写存储过程是不建议的。20年前可能还有写,现在已经基本看不到了,所以不要研究太深,也尽量不要在开发中用,因为不好维护。这里简单举一个例子就行了。开发中尽量不要用哈,能不用就不用存储过程。)

存储过程返回多结果集:

订单表:

CREATE TABLE `test_order` (
  `order_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `order_name` varchar(255) NOT NULL DEFAULT '' COMMENT '订单名字',
  PRIMARY KEY (`order_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

INSERT INTO test_order (`order_id`, `order_name`) VALUES (1, '订单1');
INSERT INTO test_order (`order_id`, `order_name`) VALUES (2, '订单2');

支付表:

CREATE TABLE `test_pay` (
  `pay_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '支付id',
  `pay_name` varchar(255) NOT NULL DEFAULT '' COMMENT '支付名字',
  `order_id` bigint(20) NOT NULL COMMENT '订单id',
  PRIMARY KEY (`pay_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='付款记录表';

INSERT INTO test_pay (`pay_id`, `pay_name`, `order_id`) VALUES (1, '支付名字1', 1);
INSERT INTO test_pay (`pay_id`, `pay_name`, `order_id`) VALUES (2, '支付名字2', 2);

物流表(一个订单有多个阶段的物流信息):

CREATE TABLE `test_flow` (
  `flow_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '物流id',
  `flow_begin_name` varchar(255) NOT NULL DEFAULT '' COMMENT '物流开始名字',
  `flow_end_name` varchar(255) NOT NULL DEFAULT '' COMMENT '物流结束名字',
  `order_id` bigint(20) NOT NULL COMMENT '订单id',
  PRIMARY KEY (`flow_id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COMMENT='物流表';

INSERT INTO test_flow (`flow_id`, `flow_begin_name`, `flow_end_name`, `order_id`) VALUES (1, '北京', '上海', 1);
INSERT INTO test_flow (`flow_id`, `flow_begin_name`, `flow_end_name`, `order_id`) VALUES (2, '上海', '浦东新区', 1);
INSERT INTO test_flow (`flow_id`, `flow_begin_name`, `flow_end_name`, `order_id`) VALUES (3, '浦东新区', '川沙新政', 1);
INSERT INTO test_flow (`flow_id`, `flow_begin_name`, `flow_end_name`, `order_id`) VALUES (4, '西藏', '黑龙江', 2);
INSERT INTO test_flow (`flow_id`, `flow_begin_name`, `flow_end_name`, `order_id`) VALUES (5, '黑龙江', '漠河', 2);
INSERT INTO test_flow (`flow_id`, `flow_begin_name`, `flow_end_name`, `order_id`) VALUES (6, '漠河', '百合路35号', 2);

1个订单对应1个支付信息、1个订单对应n个阶段的物流信息

在这里插入图片描述

创建存储过程:

-- 修改默认的语句结束符为 $$,以便在存储过程中使用分号
DELIMITER $$

-- 创建存储过程 `selectOrderAndFlow`,接受一个输入参数 `orderId`,类型为 bigint
CREATE PROCEDURE `selectOrderAndFlow`(IN `orderId` bigint)
BEGIN
    -- 查询订单信息:从 test_order 表中获取 order_id 和 order_name
	SELECT order_id, order_name FROM test_order where order_id=orderId;

    -- 查询支付信息:从 test_pay 表中获取 order_id、pay_id 和 pay_name
    SELECT order_id, pay_id, pay_name FROM test_pay where order_id=orderId;


    -- 查询流程信息:从 test_flow 表中获取 order_id、flow_id、flow_begin_name 和 flow_end_name
    SELECT order_id, flow_id, flow_begin_name, flow_end_name FROM test_flow where order_id=orderId;
END $$

-- 将语句结束符恢复为默认的分号
DELIMITER ;

在映射语句中,必须通过 resultSets 属性为每个结果集指定一个名字,多个名字使用逗号隔开:

<?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="com.yimeng.mapper.TestOrderMapper">

    <!-- 定义结果映射 -->
    <resultMap type="com.yimeng.domain.TestOrderVo2" id="testOrderVo2">
        <!--定义映射的顺序不一定要和存储过程的查询顺序一样。我们存储过程是查询test_order、test_pay、test_flow的顺序的。-->
        <!-- 映射 testPay 对象。除了存储过程查询的第一个实体类外的映射都要写resultSet、column和foreignColumn -->
        <association property="testPay"
                     javaType="com.yimeng.domain.TestPay"
                     resultSet="testPay1"
                     column="order_id"
                     foreignColumn="order_id">
            <id property="payId" column="pay_id"/>
            <result property="payName" column="pay_name"/>
            <result property="orderId" column="order_id"/>
        </association>

        <!-- 映射 testOrder 对象。存储过程的第一个查询结果会放到这个没有给resultSet的映射里面。 -->
        <association property="testOrder" javaType="com.yimeng.domain.TestOrder">
            <result property="orderId" column="order_id" />
            <result property="orderName" column="order_name" />
        </association>

        <!-- 映射 testFlowList 集合 -->
        <collection property="testFlowList"
                    ofType="com.yimeng.domain.TestFlow"
                    resultSet="testFlowList222"
                    column="order_id"
                    foreignColumn="order_id">
            <id property="flowId" column="flow_id"/>
            <result property="flowBeginName" column="flow_begin_name"/>
        </collection>

    </resultMap>

    <!-- 定义存储过程调用。存储过程第一个查询结果的resultSet随便叫,后面的都要和前面的resultSet对应起来。类型要写CALLABLE。 -->
    <select id="getOrderAndFlow"
            resultSets="aaa,testPay1,testFlowList222"
            statementType="CALLABLE"
            resultMap="testOrderVo2">
        <!--
                    调用存储过程 selectOrderAndFlow,并传入参数 id
                    - #{id}: 参数占位符,id 是传入的参数名
                    - jdbcType: 参数的数据类型,这里是 INTEGER
                    - mode: 参数模式,这里是 IN(输入参数)
                -->
        {call selectOrderAndFlow(#{id,jdbcType=INTEGER,mode=IN})}
    </select>

</mapper>
package com.yimeng.domain;

import lombok.Data;

@Data
public class TestFlow {
    private static final long serialVersionUID = 1L;

    /**
     * 物流id
     */
    private Long flowId;

    /**
     * 物流开始名字
     */
    private String flowBeginName;

    /**
     * 物流结束名字
     */
    private String flowEndName;

    /**
     * 订单id
     */
    private Long orderId;
}
package com.yimeng.domain;

import lombok.Data;

@Data
public class TestOrder {

    private Long orderId;

    private String orderName;
}
package com.yimeng.domain;

import lombok.Data;
import java.util.List;

@Data
public class TestOrderVo2 {

    /** 订单对象 */
    private TestOrder testOrder;

    /** 支付对象 */
    private TestPay testPay;

    /** 物流list */
    private List<TestFlow> testFlowList;
}
package com.yimeng.domain;

import lombok.Data;

@Data
public class TestPay {
    /**
     * 支付id
     */
    private Long payId;

    /**
     * 支付名字
     */
    private String payName;

    /**
     * 订单id
     */
    private Long orderId;
}
package com.yimeng.mapper;

import com.yimeng.domain.TestOrderVo2;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface TestOrderMapper {

    List<TestOrderVo2> getOrderAndFlow(Long id);
}
<?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="com.yimeng.mapper.TestOrderMapper">

    <!-- 定义结果映射 -->
    <resultMap type="com.yimeng.domain.TestOrderVo2" id="testOrderVo2">
        <!--定义映射的顺序不一定要和存储过程的查询顺序一样。我们存储过程是查询test_order、test_pay、test_flow的顺序的。-->
        <!-- 映射 testPay 对象。除了存储过程查询的第一个实体类外的映射都要写resultSet、column和foreignColumn -->
        <association property="testPay"
                     javaType="com.yimeng.domain.TestPay"
                     resultSet="testPay1"
                     column="order_id"
                     foreignColumn="order_id">
            <id property="payId" column="pay_id"/>
            <result property="payName" column="pay_name"/>
            <result property="orderId" column="order_id"/>
        </association>

        <!-- 映射 testOrder 对象。存储过程的第一个查询结果会放到这个没有给resultSet的映射里面。 -->
        <association property="testOrder" javaType="com.yimeng.domain.TestOrder">
            <result property="orderId" column="order_id" />
            <result property="orderName" column="order_name" />
        </association>

        <!-- 映射 testFlowList 集合 -->
        <collection property="testFlowList"
                    ofType="com.yimeng.domain.TestFlow"
                    resultSet="testFlowList222"
                    column="order_id"
                    foreignColumn="order_id">
            <id property="flowId" column="flow_id"/>
            <result property="flowBeginName" column="flow_begin_name"/>
        </collection>

    </resultMap>

    <!-- 定义存储过程调用。存储过程第一个查询结果的resultSet随便叫,后面的都要和前面的resultSet对应起来。类型要写CALLABLE。resultSets要和数据库中存储过程的顺序一样。 -->
    <select id="getOrderAndFlow"
            resultSets="aaa,testPay1,testFlowList222"
            statementType="CALLABLE"
            resultMap="testOrderVo2">
        <!--
                    调用存储过程 selectOrderAndFlow,并传入参数 id
                    - #{id}: 参数占位符,id 是传入的参数名
                    - jdbcType: 参数的数据类型,这里是 INTEGER
                    - mode: 参数模式,这里是 IN(输入参数)
                -->
        {call selectOrderAndFlow(#{id,jdbcType=INTEGER,mode=IN})}
    </select>

</mapper>

测试类:

package com.yimeng.mapper;

import com.yimeng.domain.TestOrderVo2;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;

@SpringBootTest
public class PurchaseMapperTest {

    @Resource
    private TestOrderMapper testOrderMapper;

    @Test
    public void testMybatis() {
        List<TestOrderVo2> resultList = testOrderMapper.getOrderAndFlow(1L);
        resultList.forEach((orderAndFlow)->{
            System.out.println("====================================");
            System.out.println("订单对象:"+orderAndFlow.getTestOrder());
            System.out.println("支付对象:"+orderAndFlow.getTestPay());
            System.out.println("物流列表:"+orderAndFlow.getTestFlowList());
        });
    }
}

执行结果:

====================================
订单对象:TestOrder(orderId=1, orderName=订单1)
支付对象:TestPay(payId=1, payName=支付名字1, orderId=1)
物流列表:[TestFlow(flowId=1, flowBeginName=北京, flowEndName=上海, orderId=1), TestFlow(flowId=2, flowBeginName=上海, flowEndName=浦东新区, orderId=1), TestFlow(flowId=3, flowBeginName=浦东新区, flowEndName=川沙新政, orderId=1)]

insert、update、delete

insert,update 和 delete 标签属性的使用都差不多。大部分属性和 select 元素相同,我们介绍 3 个不同的属性(select没有这三个属性)。

这三个属性是为了获取数据库自动生成的主键的。useGeneratedKeys表示告诉 MyBatis 是否要获取数据库自动生成的主键,如果是false,那么不会获取主键。如果是true,那么会获取主键,但是需要注意,数据库表的主键得要是自动生成的才行,一些数据不支持自动生成,那么就写true也无法获取到主键。keyProperty表示指定实体类的属性哪几个属性会去接收数据库的自动生成主键,如果写了useGeneratedKeys=“true”,那么keyProperty必须指定,不然无法获取到主键值。keyColumn 表示指定数据库自动生成主键的字段,这个属性可以省略不写。

  • useGeneratedKeys : (仅适用于 insert 和 update)加上这个属性会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键,默认值:false。
  • keyProperty : (仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)。如果使用了useGeneratedKeys ,那么你想要获取到自动生成的主键,那么必须指定keyProperty 。
  • keyColumn : (仅适用于 insert 和 update)指定数据库表中生成主键的列名。默认值:未设置。这个属性一般可以省略。

注意:insert、update 、delete 标签中不需要设置 resultType或者resultMap 属性,只有查询操作才需要对返回结果类型进行相应的指定。

批量插入也useGeneratedKeys 和 keyProperty 也支持获取到主键。

package com.yimeng;

import com.yimeng.domain.User;
import com.yimeng.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@SpringBootTest
public class SpringbootMybatisApplicationTests {
    @Resource
    private UserMapper userMapper;
    @Test
    public void test5() {
        User user = new User(null, "张三1", "123454", "123456789");
        User user2 = new User(null, "张三2", "123454", "123456789");
        List<User> list=new ArrayList<>();
        list.add(user);
        list.add(user2);
        userMapper.batchInsertUser(list);
        System.out.println(user.getId());// 输出57
    }
}
//@Mapper// 注册mapper接口的bean的方式3:在对应的接口上添加@Mapper注解
public interface UserMapper {
    int batchInsertUser(List<User> list);
}
<?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="com.yimeng.mapper.UserMapper">
    <insert id="batchInsertUser" useGeneratedKeys="true" keyProperty="id">
        insert into user (username, password) values
        <foreach item="item" collection="list" separator=",">
            (#{item.userName}, #{item.password})
        </foreach>
    </insert>
</mapper>
特殊情况:

如果在实际项目中,若数据库不支持主键自动递增(例如 Oracle),或者数据库支持自增主键,但是你插入数据的表中的主键没有用自增的主键。或者使用了不支持自动生成主键的 JDBC 驱动。

失效的情况:

  1. 数据库不支持主键自动递增
  2. 数据库支持自增主键,但是你插入数据的表中没有使用自动生成主键
  3. 使用了不支持自动生成主键的 JDBC 驱动
解决方案1:

那么我们可以使用 MyBatis 的<selectKey>标签自定义生成主键,具体配置代码如下。

@Test
public void test8(){
    User user = new User(null, "张三6", "123454", "123456789");
    userMapper.insertUser5(user);
    System.out.println(user.getId());// 输出7595
}
int insertUser5(User user);
<insert id="insertUser5">
    <!-- 先使用selectKey标签定义主键,然后再定义SQL语句 -->
    <selectKey keyProperty="id" resultType="Integer" order="BEFORE">
        select if(max(id) is null,1,max(id)+1) as newId from user
    </selectKey>
    insert into user (id,username, password) values (#{id},#{userName}, #{password})
</insert>

<selectKey>标签中属性说明如下:

  • keyProperty:用于指定主键值对应的 PO 类的属性。
  • order:该属性取值可以为 BEFORE 或 AFTER。BEFORE 表示先执行 <selectKey> 标签内的语句,再执行插入语句;AFTER 表示先执行插入语句再执行 <selectKey> 标签内的语句。
解决方案2:

对于不支持自动生成主键列的数据库和可能不支持自动生成主键的 JDBC 驱动,MyBatis 有另外一种方法来生成主键

这里有一个简单的示例,它可以生成一个随机 ID(不建议实际使用,这里只是为了展示 MyBatis 处理问题的灵活性和宽容度):

@Test
public void test6(){
    User user = new User(null, "张三6", "123454", "123456789");
    userMapper.insertUser3(user);
    System.out.println(user.getId());// 输出908547
}
int insertUser3(User user);
<insert id="insertUser3">
	<selectKey keyProperty="id" resultType="int" order="BEFORE">
		SELECT CAST(RAND() * 1000000 AS SIGNED) AS a FROM DUAL
	</selectKey>
	insert into user(id, username) values(#{id}, #{userName})
</insert>

在上面的示例中,首先会运行 selectKey 元素中的语句,并设置 User 的 id,然后才会调用插入语句。这样就实现了数据库自动生成主键类似的行为,同时保持了 Java 代码的简洁。

selectKey 元素属性主要有keyProperty、resultType、order、statementType、keyColumn属性。

属性名 是否必填 默认值 说明
keyProperty 指定将查询结果设置到实体类的哪个属性中。
resultType 指定查询结果的类型(如 intlongString 等)。
order 指定执行顺序,可选值为 BEFOREAFTER: - BEFORE:在插入之前执行。 - AFTER:在插入之后执行。
statementType PREPARED 指定 SQL 语句的类型,可选值为 STATEMENTPREPAREDCALLABLE
databaseId 指定数据库厂商的 ID(如 mysqloracle 等),用于多数据库支持。
keyColumn 指定数据库表中生成主键的列名(仅在某些数据库中使用)。

其中需要注意的是order 属性,selectKey 中的 order 属性有2个选择:BEFORE 和 AFTER 。

  • BEFORE:表示先执行selectKey的语句,然后将查询到的值设置到 JavaBean 对应属性上,然后再执行 insert 语句。
  • AFTER:表示先执行 插入 语句,然后再执行 selectKey 语句,并将 selectKey 得到的值设置到 JavaBean 中的属性。上面示例中如果改成 AFTER,那么插入的 id 就会是空值,但是返回的 JavaBean 属性内会有值。但是这样就做,就没有达到使用我们的算法来生成一个值作为主键,解决数据库没有自动生成主键的功能的问题了,所以一般我们都用。

sql标签(需要配合include)

Sql 标签用于定义可重用的 SQL 片段,可以在 SQL 映射文件中多次引用。Sql 标签主要的属性就是id了:

  1. id:指定 Sql 片段的唯一标识符。

sql中可以定义参数,参数值可以通过include中的refid传进来。注意,include也可以引用其他namespace的sql,但是引入其他namespace的sql需要写Mapper接口的全限定类名+sql的id。

比如:

package com.yimeng;

import com.yimeng.domain.Order;
import com.yimeng.domain.UserWithOrders;
import com.yimeng.mapper.UserOrderMapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;

@SpringBootTest
public class SpringbootMybatisApplicationTests {

    @Resource
    private UserOrderMapper userOrderMapper;

    @Test
    public void test() {
        UserWithOrders userWithOrders = userOrderMapper.selectUserWithOrdersById(1);
        System.out.println(userWithOrders);
        List<UserWithOrders> userWithOrdersList = userOrderMapper.selectAllUsersWithOrders();
        System.out.println(userWithOrdersList);
        Order order = userOrderMapper.selectOrderById(1);
        System.out.println(order);
    }
}
package com.yimeng.domain;

import lombok.Data;
import java.util.Date;

@Data
public class Order {
    private int id;
    private String orderNumber;
    private Date orderDate;
}
package com.yimeng.domain;

import lombok.Data;
import org.springframework.core.annotation.Order;
import java.util.List;

@Data
public class UserWithOrders {
    private int id;
    private String username;
    private String email;
    private List<Order> orders;
}
package com.yimeng.mapper;

public interface OrderMapper {

}
package com.yimeng.mapper;

import com.yimeng.domain.Order;
import com.yimeng.domain.UserWithOrders;
import java.util.List;

public interface UserOrderMapper {

    // 根据用户ID查询用户及其订单信息
    UserWithOrders selectUserWithOrdersById(int userId);

    // 查询所有用户及其订单信息
    List<UserWithOrders> selectAllUsersWithOrders();

    Order selectOrderById(int orderId);
}

UserOrderMapper.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="com.yimeng.mapper.UserOrderMapper">
    <!-- 结果映射 -->
    <resultMap id="UserWithOrdersResultMap" type="com.yimeng.domain.UserWithOrders">
        <id property="id" column="user_id" />
        <result property="username" column="username" />
        <result property="email" column="email" />
        <collection property="orders" ofType="com.yimeng.domain.Order">
            <id property="id" column="order_id" />
            <result property="orderNumber" column="order_number" />
            <result property="orderDate" column="order_date" />
        </collection>
    </resultMap>

<!--    &lt;!&ndash; 根据用户ID查询用户及其订单信息 &ndash;&gt;-->
<!--    <select id="selectUserWithOrdersById" resultMap="UserWithOrdersResultMap">-->
<!--        SELECT-->
<!--            u.id as user_id,-->
<!--            u.username,-->
<!--            u.email,-->
<!--            o.id as order_id,-->
<!--            o.order_number,-->
<!--            o.order_date-->
<!--        FROM-->
<!--            t_user u-->
<!--                LEFT JOIN-->
<!--            t_order o ON u.id = o.user_id-->
<!--        WHERE-->
<!--            u.id = #{userId}-->
<!--    </select>-->

    <!-- 提取查询参数 -->
    <sql id="myColumns">
        ${tableAlias}.id as ${fieldAlias}
    </sql>

    <!-- 根据用户ID查询用户及其订单信息 -->
    <select id="selectUserWithOrdersById" resultMap="UserWithOrdersResultMap">
        SELECT
            <include refid="myColumns">
                <property name="tableAlias" value="u"/>
                <property name="fieldAlias" value="user_id"/>
            </include>,
            u.username,
            u.email,
            <include refid="myColumns">
                <property name="tableAlias" value="o"/>
                <property name="fieldAlias" value="order_id"/>
            </include>,
            o.order_number,
            o.order_date
        FROM t_user u
            LEFT JOIN t_order o ON u.id = o.user_id
        WHERE u.id = #{userId}
    </select>

<!--    &lt;!&ndash; 查询所有用户及其订单信息 &ndash;&gt;-->
<!--    <select id="selectAllUsersWithOrders" resultMap="UserWithOrdersResultMap">-->
<!--        SELECT-->
<!--            u.id as user_id,-->
<!--            u.username,-->
<!--            u.email,-->
<!--            o.id as order_id,-->
<!--            o.order_number,-->
<!--            o.order_date-->
<!--        FROM-->
<!--            t_user u-->
<!--                LEFT JOIN-->
<!--            t_order o ON u.id = o.user_id-->
<!--    </select>-->

    <!-- 提取表名 -->
    <sql id="someTable">
        t_${tableName} ${tableAlias}
    </sql>

    <sql id="joinTableSql">
        <include refid="someTable">
            <property name="tableName" value="user"/>
            <property name="tableAlias" value="u"/>
        </include>
        LEFT JOIN
        <include refid="someTable">
            <property name="tableName" value="order"/>
            <property name="tableAlias" value="o"/>
        </include>
        ON ${joinCondition}
    </sql>

    <!-- 查询所有用户及其订单信息 -->
    <select id="selectAllUsersWithOrders" resultMap="UserWithOrdersResultMap">
        SELECT
        u.id as user_id,
        u.username,
        u.email,
        o.id as order_id,
        o.order_number,
        o.order_date
        FROM
        <include refid="joinTableSql">
            <property name="joinCondition" value="u.id = o.user_id"/>
        </include>
    </select>

    <select id="selectOrderById" resultType="com.yimeng.domain.Order">
        SELECT
            <include refid="com.yimeng.mapper.OrderMapper.order"></include>
        FROM t_order
            WHERE id = #{orderId}
    </select>
</mapper>

OrderMapper.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="com.yimeng.mapper.OrderMapper">
    <sql id="order">
        id, order_number, order_date
    </sql>
</mapper>

动态sql(9种)

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。

if

if标签用于根据条件生成不同的SQL语句。例如:

<select id="selectUserByNameAndAge" resultType="User">
    select * from user
    <where>
        <if test="name != null and name != ''">
            and name like concat('%', #{name}, '%')
        </if>
        <if test="age != null">
            and age = #{age}
        </if>
    </where>
</select>
choose、when、otherwise

MyBatis 中动态语句 choose-when-otherwise 类似于 Java 中的 switch-case-default 语句。由于 MyBatis 并没有为 if 提供对应的 else 标签,如果想要达到<if>...<else>...</else> </if>的效果,可以借助 <choose>、<when>、<otherwise>来实现。

例如:

<select id="selectUserByCondition" resultType="User">
    select * from user
    <where>
        <choose>
            <when test="name != null and name != ''">
                and name like concat('%', #{name}, '%')
            </when>
            <when test="age != null">
                and age = #{age}
            </when>
            <otherwise>
                and 1=1
            </otherwise>
        </choose>
    </where>
</select>

choose 标签按顺序判断其内部 when 标签中的判断条件是否成立,如果有一个成立,则执行相应的 SQL 语句,choose 执行结束;如果都不成立,则执行 otherwise 中的 SQL 语句。这类似于 Java 的 switch 语句,choose 为 switch,when 为 case,otherwise 则为 default。

include

include标签用于引入其他XML文件中定义的SQL片段。例如:

<!-- 定义可重用的 SQL 片段 -->
<sql id="userColumns">
	id, username, password, nick_name
</sql>

<select id="selectAllUsers" resultType="user">
	select <include refid="userColumns"/> from user
</select>
where

不用where的情况:

<select id="findName" resultType="String">
	SELECT stu.name FROM tab_stu stu WHERE 
	<if test="age != null">
		age = #{age}
	</if> 
	<if test="name!= null">
		AND name= #{name}
	</if> 
	<if test="class!= null">
		AND class = #{class}
	</if> 
</select>

当第一个if不满足或第一第二第三个if都不满足,会出现以下情况:

SELECT stu.name FROM tab_stu stu WHERE AND name = "小米" AND class ="1班”;
SELECT stu.name FROM tab_stu stu WHERE;

这会导致查询失败。使用where标签可以解决这个问题:

<select id="findName" resultType="String">
	SELECT stu.name FROM tab_stu stu 
	<where> 
		<if test="age != null">
			age = #{age}
		</if> 
		<if test="name!= null">
			AND name= #{name}
		</if> 
		<if test="class!= null">
			AND class = #{class}
		</if> 
	</where>
</select>

where标签内只要有内容,就会在where标签内的内容前拼接上WHERE关键字(where内只有空字符串,那么不会加上where的哈),而且,<where>标签内最后得到的内容如果是”AND”或”OR”开头的,<where>标签会自动去除AND和OR。

set

set 标签可以为 SQL 语句动态的添加 set 关键字,同时剔除追加到条件末尾多余的逗号。

<!-- 更新用户信息 -->
<update id="updateUser">
	UPDATE user
	<set>
		,username = #{userName},
<!--            <if test="userName != null and userName != ''">-->
<!--                username = #{userName},-->
<!--            </if>-->
		<if test="password != null and password != ''">
			password = #{password},
		</if>
		<if test="nickName != null and nickName != ''">
			nick_name = #{nickName},
		</if>
	</set>
	WHERE id = #{id}
</update>

set标签会在set标签内的最终内容前面加上set关键字,同时也会消除set最终内容最前面和最后面的逗号(最前面和最后面的逗号都可以清除)。

trim

trim:trim标签可实现where/set标签的功能

Trim标签有4个属性,分别为prefix、suffix、prefixOverrides、suffixOverrides:

属性 描述
prefix 表示在trim标签包裹的SQL前添加指定内容
suffix 表示在trim标签包裹的SQL末尾添加指定内容
prefixOverrides 去除sql语句前面的关键字或者字符,该关键字或者字符由prefixOverrides属性指定,假设该属性指定为"AND",当sql语句的开头为"AND",trim标签将会去除该"AND"
suffixOverrides 去除sql语句后面的关键字或者字符,该关键字或者字符由suffixOverrides属性指定(一般用于update语句if判断时去掉多余的逗号)。

代替where的例子:

<select id="findUserByUserNameOrNickName" resultType="user">
	SELECT id, username, password, nick_name FROM user
	<trim prefix="where" prefixOverrides="and |or">
		<if test="userName != null">
			AND username = #{userName}
		</if>
		<if test="nickName!= null">
			OR nick_name= #{nickName}
		</if>
	</trim>
</select>

代替set的例子:

<update id="updateUserById">
	update user
	<trim prefix="set" suffix="where id=#{id}" suffixOverrides=",">
		<if test="userName != null"> username=#{userName},</if>
		<if test="nickName != null"> nick_name=#{nickName},</if>
	</trim>
</update>

例子:

<!-- 使用 trim 标签 -->
<select id="selectUsersByConditionWithTrim" resultType="com.yimeng.domain.User">
    SELECT id, username, password, nick_name
    FROM user
    <trim prefix="WHERE" prefixOverrides="AND|OR " suffixOverrides=",|;">
        <if test="id != null">
            AND id = #{id}
        </if>
        <if test="userName != null and userName != ''">
            AND username LIKE CONCAT('%', #{userName}, '%')
        </if>
        <if test="nickName != null and nickName != ''">
            AND nick_name LIKE CONCAT('%', #{nickName}, '%')
        </if>
        ;
    </trim>
</select>

上面这个selectUsersByConditionWithTrim方法最终执行的sql为:SELECT id, username, password, nick_name FROM user WHERE id = 908569 AND username LIKE CONCAT(’%’, ‘user’, ‘%’)

prefixOverrides="AND |OR "表示去除最前面的and或者or。但是注意,不能写成prefixOverrides="AND | OR “。OR前面是没有空格的,有空格会报错的。AND和|之间有空格和没有空格都行的。suffixOverrides=”,|;“可以去除trim拼接结果结尾的逗号或者分号。注意哈,suffixOverrides=”,|;“不会去除suffix=”;"的分号。比如下面这样写,最终执行的结果是SELECT id, username, password, nick_name FROM user WHERE id = 908569 AND username LIKE CONCAT(’%’, ‘user’, ‘%’) ;有分号的。

<!-- 使用 trim 标签 -->
<select id="selectUsersByConditionWithTrim" resultType="com.yimeng.domain.User">
    SELECT id, username, password, nick_name
    FROM user
    <trim prefix="WHERE" prefixOverrides="AND|OR " suffixOverrides=",|;" suffix=";">
        <if test="id != null">
            AND id = #{id}
        </if>
        <if test="userName != null and userName != ''">
            AND username LIKE CONCAT('%', #{userName}, '%')
        </if>
        <if test="nickName != null and nickName != ''">
            AND nick_name LIKE CONCAT('%', #{nickName}, '%')
        </if>
    </trim>
</select>

注意:|符号被称为管道符。

foreach

foreach标签用于循环生成SQL语句。例如:

<select id="selectUsersByIds" resultType="user">
	select id, username, password, nick_name from user where id in
	<foreach item="id" index="index" collection="list" open="(" separator="," close=")">
		#{id}
	</foreach>
</select>

下面是foreach标签的各个属性:

  1. collection:迭代集合的名称,可以使用@Param注解指定,该参数为必选。如果是List可以不写@Param,直接写list,与参数名无关(但是可能这个和mybatis的版本有关,所以建议还是在mapper接口的参数前面加上@Param,然后mapper映射文件中写@Param指定的名字)。同理,如果是数组可以直接写array。但是如果是Set,不能直接写set,而是要写collection。如果是Map,就需要使用@Param指定别名了,我试过用collection=“map”;这样写没有用。总之建议使用@Param(“XX”),然后写这个XX,因为不同版本的mybatis对集合、数组的默认别名不一样,所以最好我们去指定一个别名。
  2. item:用于设置迭代内容的别名,若collection为List、Set或数组,则表示其中元素;若collection为Map,则代表key-value的value,该参数为必选。
  3. index:在List和数组中,index表示当前迭代的位置,在Map中,index指元素的key,该参数是可选项。一般我们省略,因为一般情况下都用不到。
  4. open:表示该语句以什么开始,最常使用的是左括弧”(”,MyBatis会将该字符拼接到foreach标签包裹的SQL语句之前,并且只拼接一次,该参数是可选项
  5. close:表示该语句以什么结束,最常使用的是右括弧”)”,MyBatis会将该字符拼接到foreach标签包裹的SQL语句末尾,该参数是可选项
  6. separator:表示给拼接元素时会自动在元素与元素之间插入的分割符,该参数是可选项
bind

每个数据库的拼接函数或连接符号都不同,例如 MySQL的 concat 函数、Oracle 的连接符号“||”等。这样 SQL 映射文件就需要根据不同的数据库提供不同的实现,显然比较麻烦,且不利于代码的移植。幸运的是,MyBatis 提供了 bind 标签来解决这一问题。

bind 标签可以通过 OGNL 表达式自定义一个上下文变量。

比如,按照用户昵称进行模糊查询,SQL 映射文件如下。

<!-- 使用 bind 标签 -->
<select id="selectUsersByNickName" resultType="com.yimeng.domain.User">
	<bind name="pattern" value="'%' + nickName + '%'"/>
	SELECT id, username, password, nick_name FROM user
	WHERE nick_name LIKE #{pattern}
</select>

bind 元素属性如下。

  • value:对应传入实体类的某个字段,可以进行字符串拼接等特殊处理。
  • name:给对应参数取的别名。

以上代码中的“nickName”代表传递进来的参数,它和通配符连接后,赋给了 pattern,然后就可以在 select 语句中使用pattern这个变量进行模糊查询,不管是 MySQL 数据库还是 Oracle 数据库都可以使用这样的语句,提高了可移植性。

大部分情况下需要传递多个参数,下面为传递多个参数时 bind 的用法示例。

<!-- 使用 bind 标签 -->
<select id="selectUsersByUserNameAndNickName" resultType="com.yimeng.domain.User">
	<bind name="pattern_username" value="'%' + username + '%'"/>
	<bind name="pattern_nick_name" value="'%' + nickName + '%'"/>
	SELECT id, username, password, nick_name FROM user WHERE nick_name LIKE #{pattern_nick_name} AND username LIKE #{pattern_username}
</select>

2、cache、cache-ref

前面讲过了。这里不讲了。

3、parameterMap

已经弃用。不讲。

4、resultMap

这里说的resultMap是一个标签,而不是select中的resultMap属性。但是要讲resultMap这个标签,就不得不讲select标签的映射了。select标签可以支持使用resultType属性的简单映射,也支持使用resultMap属性指定一个resultMap的id的复杂映射。因为要讲resultMap就绕不开select标签,所以,我们下面是select标签和resultMap标签一起配合着讲的。

在select语句中,resultType和resultMap属性必须选择一个,并且只能选择一个,不然mybatis不知道把结果保存到什么里面了。而像insert、update、delete中就没有说要指定resultType或者resultMap属性了。

i、resultMap标签的使用

select标签的resultType属性

没有开启驼峰命名的情况下,查询结果的字段名和java属性名完全一样,那么使用resultType完成匹配(mysql默认不区分大小写,所以你可能会看到sql查询的结果字段名是usernamE,却可以匹配上java中userName的情况,但是数据库查询结果的nick_name不能匹配上java中的nickName,java中写nick_name才能匹配上数据库中的nick_name,当然数据库查询结果的nick_naMe,也能匹配上java中的nIck_namE。mybatis匹配时不区分大小写嘛。)。

如果开启驼峰命名,sql的查询结果nick_name可以匹配上java中的nickName,但是数据库查询结果的nick_name不能匹配上java中的nick_name了。数据库查询结果的nick_name匹配nickNaMe还是可以的。mybatis匹配时不区分大小写嘛。

resultType后面写的是实体类的全限定类名,用来接收返回结果。resultType后面可以写类的别名。别名在配置文件中配置。

总结:resultType只能映射查询结果和java字段完全匹配的数据。并且匹配的时候不会区分大小写。如果你开启了驼峰命名,那么匹配规则就不是完全匹配了,而是改为了驼峰匹配,只有满足驼峰匹配的resultType才能自动映射,这时你数据库的nick_name就不能映射java中的nick_name了。

select标签的resultMap属性配合resultMap标签

如果存在不能使用resultType匹配,你就需要使用resultMap标签来手动绑定映射关系了。使用resultMap的时候,可以只写不能自动匹配的映射。然后select标签的resultMap属性需要指向resultMap标签的id。一个namespace的select标签的resultMap属性也可以指向另一个namespace中的resultMap标签的,但是如果这个namespace指向另一个namespace的resultMap标签,那么这个resultMap属性你需要写"那个被指向的namespace对应接口的全限定类名.resultMap的id",而如果select标签和要指向的resultMap标签在一个namespace中,那么resultMap属性中你可以省略写全限定类名,可以直接写resultMap标签的id。

比如,如果下面的id、userName、password对于resultType来说都能自动映射,只有nick_name不能自动映射,那么你可以只写nickName对于nick_name的映射关系。这样,id、userName、password、nick_name都能成功映射。因为mybatis底层是先使用自动映射,在自动映射处理完毕后,再使用手动映射的。

<?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="com.yimeng.mapper.UserMapper">

    <!-- 定义 resultMap -->
    <resultMap id="UserResultMap" type="user">
<!--        <id property="id" column="id" />-->
<!--        <result property="userName" column="username" />-->
<!--        <result property="password" column="password" />-->
        <result property="nickName" column="nick_name" />
    </resultMap>

    <!-- 根据用户ID查询用户 -->
    <select id="selectUserById" resultMap="UserResultMap">
        SELECT id, usernamE, password, nick_name FROM user WHERE id = #{id}
    </select>
</mapper>
select标签配合resultMap标签完成复杂映射

级联关系是一个数据库实体的概念,有 3 种级联关系,分别是一对一级联、一对多级联以及多对多级联。

  • 一对一级联:一个表中的一条记录只对应另一个表中的一条记录。示例:用户表 和 用户详细信息表,每个用户只有一条详细信息记录。

  • 一对多级联:一个表中的一条记录对应另一个表中的多条记录。示例:订单表 和 订单项表,一个订单可以有多个订单项。

  • 多对多级联:多个表中的记录相互关联,通常通过中间表实现。示例:学生表 和 课程表,一个学生可以选修多门课程,一门课程也可以被多个学生选修。

    -- 查询某个学生选修的所有课程
    SELECT s.student_name, c.course_name
    FROM students s
    JOIN student_courses sc ON s.student_id = sc.student_id
    JOIN courses c ON sc.course_id = c.course_id
    WHERE s.student_id = 10;
    
    -- 查询某门课程的所有学生
    SELECT c.course_name, s.student_name
    FROM courses c
    JOIN student_courses sc ON c.course_id = sc.course_id
    JOIN students s ON sc.student_id = s.student_id
    WHERE c.course_id = 101;
    
一对一

通过 <resultMap> 元素的子元素 <association> 处理一对一级联关系。

<association> 元素中通常使用以下属性:

  • property:指定映射到实体类的对象属性名。
  • column:指定表中对应的字段名(即查询sql结果返回的列名)。
  • javaType:指定映射到实体对象属性的类型。
  • select:指定引入嵌套查询的子 SQL 语句,该属性用于关联映射中的嵌套查询。

环境准备:

/*
SQLyog Ultimate v10.00 Beta1
MySQL - 8.0.30 : Database - mybatis-label
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`mybatis-label` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;

USE `mybatis-label`;

/*Table structure for table `student` */

DROP TABLE IF EXISTS `student`;

CREATE TABLE `student` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `sex` tinyint DEFAULT NULL,
  `card_id` int DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `card_id` (`card_id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb3;

/*Data for the table `student` */

insert  into `student`(`id`,`name`,`sex`,`card_id`) values (1,'C语言中文网',0,2),(2,'编程帮',0,1),(3,'赵小红',1,3),(4,'李晓明',0,4),(5,'李紫薇',1,5);

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*
SQLyog Ultimate v10.00 Beta1
MySQL - 8.0.30 : Database - mybatis-label
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`mybatis-label` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;

USE `mybatis-label`;

/*Table structure for table `student_card` */

DROP TABLE IF EXISTS `student_card`;

CREATE TABLE `student_card` (
  `id` int NOT NULL AUTO_INCREMENT,
  `start_date` date DEFAULT NULL,
  `end_date` date DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb3;

/*Data for the table `student_card` */

insert  into `student_card`(`id`,`start_date`,`end_date`) values (1,'2021-03-01','2021-03-11'),(2,'2021-03-02','2021-03-12'),(3,'2021-03-03','2021-03-13'),(4,'2021-03-04','2021-03-14'),(5,'2021-03-05','2021-03-15');

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

image-20250224224614997

image-20250224224541146

实体类:

package com.yimeng.domain;

import lombok.Data;

@Data
public class Student {
    private int id;
    private String name;
    private int sex;
    private StudentCard studentCard;
}
package com.yimeng.domain;

import lombok.Data;
import java.util.Date;

@Data
public class StudentCard {
    private int id;
    private Date startDate;
    private Date endDate;
}
分步查询
  • 分步查询,通过两次或多次查询,为一对一关系的实体 Bean 赋值
package com.yimeng;

import com.yimeng.domain.Student;
import com.yimeng.mapper.StudentMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;

@SpringBootTest
public class SpringbootMybatisApplicationTests {

    @Resource
    private StudentMapper studentMapper;

    // 分步查询(正确)
    @Test
    public void test() {
        Student student = studentMapper.selectStuById1(1);
        System.out.println(student);
        // Student(id=1, name=C语言中文网, sex=0, studentCard=StudentCard(id=2, startDate=Tue Mar 02 00:00:00 CST 2021, endDate=Fri Mar 12 00:00:00 CST 2021))
    }
}

StudentMapper.java:

package com.yimeng.mapper;

import com.yimeng.domain.Student;

public interface StudentMapper {
    public Student selectStuById1(int id);
}

StudentMapper.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="com.yimeng.mapper.StudentMapper">

    <!-- 一对一根据id查询学生信息:级联查询的第一种方法(嵌套查询,执行两个SQL语句) -->
    <resultMap type="com.yimeng.domain.Student" id="cardAndStu1">
        <id property="id" column="id" />
        <result property="name" column="name" />
        <result property="sex" column="sex" />
        <!-- 一对一级联查询。把查询的card_id作为参数去调用com.yimeng.mapper.StudentCardMapper.selectStuCardById方法,并且把结果封装到com.yimeng.domain.StudentCard后放到com.yimeng.domain.Student的studentCard字段中。 -->
        <association property="studentCard" column="card_id"
                     javaType="com.yimeng.domain.StudentCard"
                     select="com.yimeng.mapper.StudentCardMapper.selectStuCardById" />
    </resultMap>

    <select id="selectStuById1" parameterType="Integer"
            resultMap="cardAndStu1">
        select * from student where id=#{id}
    </select>
</mapper>

StudentCardMapper.java:

package com.yimeng.mapper;

import com.yimeng.domain.StudentCard;

public interface StudentCardMapper {
    public StudentCard selectStuCardById(int id);
}

StudentCardMapper.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="com.yimeng.mapper.StudentCardMapper">

    <select id="selectStuCardById" resultType="com.yimeng.domain.StudentCard">
        SELECT * FROM student_card WHERE id = #{id}
    </select>
</mapper>
单步查询
  • 单步查询,通过关联查询实现
package com.yimeng;

import com.yimeng.domain.Student;
import com.yimeng.mapper.StudentMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;

@SpringBootTest
public class SpringbootMybatisApplicationTests {

    @Resource
    private StudentMapper studentMapper;

    // 单步查询(正确演示)
    @Test
    public void test4() {
        Student student = studentMapper.selectStuById4(1);
        System.out.println(student);
        // Student(id=1, name=C语言中文网, sex=0, studentCard=StudentCard(id=2, startDate=Tue Mar 02 00:00:00 CST 2021, endDate=null))
    }
}
package com.yimeng.mapper;

import com.yimeng.domain.Student;

public interface StudentMapper {

    public Student selectStuById4(int id);
}
<?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="com.yimeng.mapper.StudentMapper">

    <!--正确的写法。-->
    <resultMap type="com.yimeng.domain.Student" id="cardAndStu4">
        <id property="id" column="id" />
        <result property="name" column="name" />
        <result property="sex" column="sex" />
        <association property="studentCard"
                     javaType="com.yimeng.domain.StudentCard">
            <id property="id" column="sc_id" />
            <result property="startDate" column="start_date" />
            <!--映射得写,不写不能自动映射。-->
<!--            <result property="endDate" column="end_date" />-->
        </association>
    </resultMap>

    <!--因为s写在前面,所以id是s.id的值,所以<id property="id" column="id" />能赋值为正确的值。因为<id property="id" column="sc_id" />,所以studentCard的id会被赋值为sc_id的值。-->
    <select id="selectStuById4" parameterType="Integer"
            resultMap="cardAndStu4">
        SELECT s.*,sc.*,sc.id as sc_id FROM student s,student_card sc
        WHERE s.card_id = sc.id AND s.id=#{id}
    </select>
</mapper>
注意点

注意:单步查询,如果你查询的结果有两个id列,那么第一个id列才会被识别。你可以通过设置别名解决这个问题。

注意:association内的javaType中的类型和数据库之间不能自动映射。所以建议resultMap的映射关系不要省略,不用他的自动映射,映射全部自己写。

完整案例(包括前面的正确案例。Student和StudentCard还是和上面一样):

package com.yimeng.mapper;

import com.yimeng.domain.StudentCard;

public interface StudentCardMapper {
    public StudentCard selectStuCardById(int id);
}
package com.yimeng.mapper;

import com.yimeng.domain.Student;

public interface StudentMapper {
    public Student selectStuById1(int id);

    public Student selectStuById2(int id);

    public Student selectStuById3(int id);

    public Student selectStuById4(int id);
}
<?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="com.yimeng.mapper.StudentCardMapper">

    <select id="selectStuCardById" resultType="com.yimeng.domain.StudentCard">
        SELECT * FROM student_card WHERE id = #{id}
    </select>
</mapper>
<?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="com.yimeng.mapper.StudentMapper">

    <!-- 一对一根据id查询学生信息:级联查询的第一种方法(嵌套查询,执行两个SQL语句) -->
    <resultMap type="com.yimeng.domain.Student" id="cardAndStu1">
        <id property="id" column="id" />
        <result property="name" column="name" />
        <result property="sex" column="sex" />
        <!-- 一对一级联查询。把查询的card_id作为参数去调用com.yimeng.mapper.StudentCardMapper.selectStuCardById方法,并且把结果封装到com.yimeng.domain.StudentCard后放到com.yimeng.domain.Student的studentCard字段中。 -->
        <association property="studentCard" column="card_id"
                     javaType="com.yimeng.domain.StudentCard"
                     select="com.yimeng.mapper.StudentCardMapper.selectStuCardById" />
    </resultMap>

    <select id="selectStuById1" parameterType="Integer"
            resultMap="cardAndStu1">
        select * from student where id=#{id}
    </select>

    <!--错误的案例。-->
    <resultMap type="com.yimeng.domain.Student" id="cardAndStu2">
        <id property="id" column="id" />
        <result property="name" column="name" />
        <result property="sex" column="sex" />
        <!-- 一对一级联查询。 -->
        <association property="studentCard"
                     javaType="com.yimeng.domain.StudentCard">
            <id property="id" column="id" />
            <result property="startDate" column="start_date" />
            <!--映射得写,不写不能自动映射。-->
<!--            <result property="endDate" column="end_date" />-->
        </association>
    </resultMap>

    <!--注意,因为这个查询有s.id和sc.id,查询结果的列名都叫id,这样只会认第一个id。而cardAndStu2中上面这样写resultMap的意义是:“是把id的值,放到student的id和studentCard的id中”,所以mybatis会把s.id给到student的id和studentCard的id中,这样的结果就是错的(studentCard中的id属性需要的是sc表的id)-->
    <select id="selectStuById2" parameterType="Integer"
            resultMap="cardAndStu2">
        SELECT s.*,sc.* FROM student s,student_card sc
        WHERE s.card_id = sc.id AND s.id=#{id}
    </select>

    <!--测试换位置。-->
    <resultMap type="com.yimeng.domain.Student" id="cardAndStu3">
        <id property="id" column="id" />
        <result property="name" column="name" />
        <result property="sex" column="sex" />
        <!-- 一对一级联查询。 -->
        <association property="studentCard"
                     javaType="com.yimeng.domain.StudentCard">
            <id property="id" column="id" />
            <result property="startDate" column="start_date" />
            <!--映射得写,不写不能自动映射。-->
<!--            <result property="endDate" column="end_date" />-->
        </association>
    </resultMap>

    <!--因为sc写在前面,所以sc.id会被mybatis识别为id。-->
    <select id="selectStuById3" parameterType="Integer"
            resultMap="cardAndStu3">
        SELECT sc.*,s.* FROM student s,student_card sc
        WHERE s.card_id = sc.id AND s.id=#{id}
    </select>


    <!--正确的写法。-->
    <resultMap type="com.yimeng.domain.Student" id="cardAndStu4">
        <id property="id" column="id" />
        <result property="name" column="name" />
        <result property="sex" column="sex" />
        <association property="studentCard"
                     javaType="com.yimeng.domain.StudentCard">
            <id property="id" column="sc_id" />
            <result property="startDate" column="start_date" />
            <!--映射得写,不写不能自动映射。-->
<!--            <result property="endDate" column="end_date" />-->
        </association>
    </resultMap>

    <!--因为s写在前面,所以id是s.id的值,所以<id property="id" column="id" />能赋值为正确的值。因为<id property="id" column="sc_id" />,所以studentCard的id会被赋值为sc_id的值。-->
    <select id="selectStuById4" parameterType="Integer"
            resultMap="cardAndStu4">
        SELECT s.*,sc.*,sc.id as sc_id FROM student s,student_card sc
        WHERE s.card_id = sc.id AND s.id=#{id}
    </select>
</mapper>
package com.yimeng;

import com.yimeng.domain.Student;
import com.yimeng.mapper.StudentMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;

@SpringBootTest
public class SpringbootMybatisApplicationTests {

    @Resource
    private StudentMapper studentMapper;

    // 分步查询(正确)
    @Test
    public void test() {
        Student student = studentMapper.selectStuById1(1);
        System.out.println(student);
        // Student(id=1, name=C语言中文网, sex=0, studentCard=StudentCard(id=2, startDate=Tue Mar 02 00:00:00 CST 2021, endDate=Fri Mar 12 00:00:00 CST 2021))
    }

    // 单步查询(错误演示)
    @Test
    public void test2() {
        Student student = studentMapper.selectStuById2(1);
        System.out.println(student);
        // Student(id=1, name=C语言中文网, sex=0, studentCard=StudentCard(id=1, startDate=Tue Mar 02 00:00:00 CST 2021, endDate=null))
    }

    // 单步查询(错误演示).因为sc写在前面,所以sc.id会被mybatis识别为id,映射的时候,Student和StudentCard的id都是拿id的值,所以看到实体类的Student和StudentCard的id都是2
    @Test
    public void test3() {
        Student student = studentMapper.selectStuById3(1);
        System.out.println(student);
        // Student(id=2, name=C语言中文网, sex=0, studentCard=StudentCard(id=2, startDate=Tue Mar 02 00:00:00 CST 2021, endDate=null))
    }

    // 单步查询(正确演示)
    @Test
    public void test4() {
        Student student = studentMapper.selectStuById4(1);
        System.out.println(student);
        // Student(id=1, name=C语言中文网, sex=0, studentCard=StudentCard(id=2, startDate=Tue Mar 02 00:00:00 CST 2021, endDate=null))
    }
}
一对多

在实际生活中有许多一对多级联关系,例如一个用户可以有多个订单,而一个订单只属于一个用户。

通过 <resultMap> 元素的子元素 <collection> 处理一对多级联关系,collection 可以将关联查询的多条记录映射到一个 list 集合属性中。

<association> 元素中通常使用以下属性:

  • property:指定映射到实体类的对象属性名。
  • column:指定表中对应的字段名(即查询sql结果返回的列名)。
  • javaType:指定映射到实体对象属性的类型。
  • select:指定引入嵌套查询的子 SQL 语句,该属性用于关联映射中的嵌套查询。

下面以用户和订单为例讲解一对多关联查询(实现“根据 id 查询用户及其关联的订单信息”的功能)的处理过程。

环境准备:

/*
SQLyog Ultimate v10.00 Beta1
MySQL - 8.0.30 : Database - mybatis-label
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`mybatis-label` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;

USE `mybatis-label`;

/*Table structure for table `t_order` */

DROP TABLE IF EXISTS `t_order`;

CREATE TABLE `t_order` (
  `id` int NOT NULL AUTO_INCREMENT,
  `order_number` varchar(50) NOT NULL,
  `user_id` int NOT NULL,
  `order_date` date NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

/*Data for the table `t_order` */

insert  into `t_order`(`id`,`order_number`,`user_id`,`order_date`) values (1,'ORD1001',1,'2023-10-01'),(2,'ORD1002',1,'2023-10-02'),(3,'ORD1003',2,'2023-10-03'),(4,'ORD1004',2,'2023-10-04'),(5,'ORD1005',3,'2023-10-05'),(6,'ORD1006',3,'2023-10-06'),(7,'ORD1007',4,'2023-10-07'),(8,'ORD1008',4,'2023-10-08'),(9,'ORD1009',5,'2023-10-09'),(10,'ORD1010',5,'2023-10-10');

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*
SQLyog Ultimate v10.00 Beta1
MySQL - 8.0.30 : Database - mybatis-label
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`mybatis-label` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;

USE `mybatis-label`;

/*Table structure for table `t_user` */

DROP TABLE IF EXISTS `t_user`;

CREATE TABLE `t_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `email` varchar(100) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

/*Data for the table `t_user` */

insert  into `t_user`(`id`,`username`,`email`) values (1,'Alice','alice@example.com'),(2,'Bob','bob@example.com'),(3,'Charlie','charlie@example.com'),(4,'David','david@example.com'),(5,'Eve','eve@example.com');

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

image-20250225222643613

image-20250225222652171

实体类:

package com.yimeng.domain;

import lombok.Data;
import java.util.Date;

@Data
public class Order {
    private int id;
    private String orderNumber;
    private Long userId;
    private Date orderDate;
}
package com.yimeng.domain;

import lombok.Data;
import java.util.List;

@Data
public class User {
    private Integer id;
    private String username;
    private String email;
    private List<Order> orderList;
}
分步查询
package com.yimeng.mapper;

import com.yimeng.domain.Order;
import org.apache.ibatis.annotations.Param;
import java.util.List;

public interface OrderMapper {
    public List<Order> selectOrderByUserId(@Param("uid") int uid);
}
<?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="com.yimeng.mapper.OrderMapper">

    <!-- 根据id查询订单信息 -->
    <select id="selectOrderByUserId" resultType="com.yimeng.domain.Order"
            parameterType="Integer">
        SELECT * FROM `t_order` where user_id=#{uid}
    </select>

</mapper>
package com.yimeng.mapper;

import com.yimeng.domain.User;
import org.apache.ibatis.annotations.Param;

public interface UserMapper {
    public User selectUserOrderById1(@Param("id") int id);
}
<?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="com.yimeng.mapper.UserMapper">

    <!-- 一对多 根据id查询用户及其关联的订单信息:级联查询的第一种方法(分步查询) -->
    <resultMap type="com.yimeng.domain.User" id="userAndOrder1">
        <id property="id" column="id" />
        <result property="username" column="username" />
        <result property="email" column="email" />
        <!-- 一对多级联查询,ofType表示集合中的元素类型,将id传递给selectOrderById(相当于使用 select * from t_user where id=#{id} 查询到的id作为参数去使用com.yimeng.mapper.OrderMapper.selectOrderByUserId进行查询,查询的多条记录封装到com.yimeng.domain.Order中并放到orderList字段里面去) -->
        <collection property="orderList"
                    ofType="com.yimeng.domain.Order" column="id"
                    select="com.yimeng.mapper.OrderMapper.selectOrderByUserId" />
    </resultMap>
    
    <select id="selectUserOrderById1" parameterType="Integer"
            resultMap="userAndOrder1">
        select * from t_user where id=#{id}
    </select>

</mapper>
package com.yimeng;

import com.yimeng.domain.User;
import com.yimeng.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;

@SpringBootTest
public class SpringbootMybatisApplicationTests {

    @Resource
    private UserMapper userMapper;

    /* 测试分步查询 */
    @Test
    public void test1() {
        User user = userMapper.selectUserOrderById1(1);
        System.out.println(user);//User(id=1, username=Alice, email=alice@example.com, orderList=[Order(id=1, orderNumber=ORD1001, userId=1, orderDate=Sun Oct 01 00:00:00 CST 2023), Order(id=2, orderNumber=ORD1002, userId=1, orderDate=Mon Oct 02 00:00:00 CST 2023)])
    }
}
单步查询
<?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="com.yimeng.mapper.UserMapper">
    
  	<!-- 一对多 根据id查询用户及其关联的订单信息:级联查询的第二种方法(单步查询) -->
    <resultMap type="com.yimeng.domain.User" id="userAndOrder2">
        <id property="id" column="id" />
        <result property="username" column="username" />
        <result property="email" column="email" />
        <!-- 一对多级联查询,ofType表示集合中的元素类型。这里的自动封装逻辑是:看查询的结果,如果id,username,email一样(外层的映射),就当作是一个User。不同的属性会封装为Order(内层的映射),并放到orderList属性中去 -->
        <collection property="orderList"
                    ofType="com.yimeng.domain.Order">
            <id property="id" column="oId" />
            <result property="orderNumber" column="order_number" />
            <result property="userId" column="user_id" />
            <result property="orderDate" column="order_date" />
        </collection>
    </resultMap>

    <select id="selectUserOrderById2" resultMap="userAndOrder2">
        SELECT u.*, o.*, o.id as oId
        FROM t_user u
                 LEFT JOIN t_order o ON u.id = o.user_id
        WHERE u.id = #{id}
    </select>

</mapper>
package com.yimeng.mapper;

import com.yimeng.domain.User;
import org.apache.ibatis.annotations.Param;

public interface UserMapper {

    public User selectUserOrderById2(@Param("id") int id);
}
package com.yimeng;

import com.yimeng.domain.User;
import com.yimeng.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;

@SpringBootTest
public class SpringbootMybatisApplicationTests {

    @Resource
    private UserMapper userMapper;

    /* 测试单步查询 */
    @Test
    public void test2() {
        User user = userMapper.selectUserOrderById2(2);
        System.out.println(user);// User(id=2, username=Bob, email=bob@example.com, orderList=[Order(id=3, orderNumber=ORD1003, userId=2, orderDate=Tue Oct 03 00:00:00 CST 2023), Order(id=4, orderNumber=ORD1004, userId=2, orderDate=Wed Oct 04 00:00:00 CST 2023)])
    }
}

测试单步多表查询的逻辑:

/*
SQLyog Ultimate v10.00 Beta1
MySQL - 8.0.30 : Database - mybatis-label
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`mybatis-label` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;

USE `mybatis-label`;

/*Table structure for table `t_user_order` */

DROP TABLE IF EXISTS `t_user_order`;

CREATE TABLE `t_user_order` (
  `id` int DEFAULT NULL,
  `username` varchar(50) DEFAULT NULL,
  `email` varchar(100) DEFAULT NULL,
  `order_number` varchar(50) DEFAULT NULL,
  `user_id` int DEFAULT NULL,
  `order_date` date DEFAULT NULL,
  `oId` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

/*Data for the table `t_user_order` */

insert  into `t_user_order`(`id`,`username`,`email`,`order_number`,`user_id`,`order_date`,`oId`) values (1,'Alice','alice@example.com','ORD1002',1,'2023-10-02',2),(1,'Alice','alice@example.com','ORD1001',1,'2023-10-01',1),(2,'Bob','bob@example.com','ORD1004',2,'2023-10-04',4),(2,'Bob','bob@example.com','ORD1003',2,'2023-10-03',3),(3,'Charlie','charlie@example.com','ORD1006',3,'2023-10-06',6),(3,'Charlie','charlie@example.com','ORD1005',3,'2023-10-05',5),(4,'David','david@example.com','ORD1008',4,'2023-10-08',8),(4,'David','david@example.com','ORD1007',4,'2023-10-07',7),(5,'Eve','eve@example.com','ORD1010',5,'2023-10-10',10),(5,'Eve','eve@example.com','ORD1009',5,'2023-10-09',9);

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

image-20250225222927695

<?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="com.yimeng.mapper.UserMapper">
    
    <!-- 一对多 根据id查询用户及其关联的订单信息:级联查询的第二种方法(单步查询) -->
    <resultMap type="com.yimeng.domain.User" id="userAndOrder2">
        <id property="id" column="id" />
        <result property="username" column="username" />
        <result property="email" column="email" />
        <!-- 一对多级联查询,ofType表示集合中的元素类型。这里的自动封装逻辑是:看查询的结果,如果id,username,email一样(外层的映射),就当作是一个User。不同的属性会封装为Order(内层的映射),并放到orderList属性中去 -->
        <collection property="orderList"
                    ofType="com.yimeng.domain.Order">
            <id property="id" column="oId" />
            <result property="orderNumber" column="order_number" />
            <result property="userId" column="user_id" />
            <result property="orderDate" column="order_date" />
        </collection>
    </resultMap>

    <select id="selectUserOrderById2" resultMap="userAndOrder2">
        SELECT u.*, o.*, o.id as oId
        FROM t_user u
                 LEFT JOIN t_order o ON u.id = o.user_id
        WHERE u.id = #{id}
    </select>

    <!-- 怎么证明上面的结论呢,可以看下面,这个表没有进行多表查询,一样可以拿到和多表查询一样的结果,因为他的查询结果是和上面多表查询的结果一样的。“这里的自动封装逻辑是:看查询的结果,如果id,username,email一样(外层的映射),就当作是一个User。不同的属性会封装为Order(内层的映射),并放到orderList属性中去” -->
    <select id="selectUserOrderById3" resultMap="userAndOrder2">
        SELECT * from t_user_order WHERE id = #{id}
    </select>
</mapper>
package com.yimeng.mapper;

import com.yimeng.domain.User;
import org.apache.ibatis.annotations.Param;

public interface UserMapper {
    public User selectUserOrderById2(@Param("id") int id);

    public User selectUserOrderById3(@Param("id") int id);
}
package com.yimeng;

import com.yimeng.domain.User;
import com.yimeng.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;

@SpringBootTest
public class SpringbootMybatisApplicationTests {

    @Resource
    private UserMapper userMapper;

    /* 测试单步查询 */
    @Test
    public void test2() {
        User user = userMapper.selectUserOrderById2(2);
        System.out.println(user);// User(id=2, username=Bob, email=bob@example.com, orderList=[Order(id=3, orderNumber=ORD1003, userId=2, orderDate=Tue Oct 03 00:00:00 CST 2023), Order(id=4, orderNumber=ORD1004, userId=2, orderDate=Wed Oct 04 00:00:00 CST 2023)])
    }

    /* 测试单步查询封装的逻辑 */
    @Test
    public void test3() {
        User user = userMapper.selectUserOrderById3(2);
        System.out.println(user);// User(id=2, username=Bob, email=bob@example.com, orderList=[Order(id=3, orderNumber=ORD1003, userId=2, orderDate=Tue Oct 03 00:00:00 CST 2023), Order(id=4, orderNumber=ORD1004, userId=2, orderDate=Wed Oct 04 00:00:00 CST 2023)])
    }
}
多对多

实际应用中,由于多对多的关系比较复杂,会增加理解和关联的复杂度,所以应用较少。MyBatis 没有实现多对多级联,推荐通过两个一对多级联替换多对多级联,以降低关系的复杂度,简化程序。

例如,一个订单可以有多种商品,一种商品可以对应多个订单,订单与商品就是多对多的级联关系。可以使用一个==中间表(订单记录表)==将多对多级联转换成两个一对多的关系。

下面以订单和商品(实现“查询所有订单以及每个订单对应的商品信息”的功能)为例讲解多对多关联查询。

CREATE TABLE `order` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `order_num` INT(25) DEFAULT NULL,
  `user_id` INT(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

INSERT INTO `order`(`id`, `order_num`, `user_id`) VALUES 
(1, 20200107, 1),
(2, 20200806, 2),
(3, 20206702, 3),
(4, 20200645, 1),
(5, 20200711, 2),
(6, 20200811, 2),
(7, 20201422, 3),
(8, 20201688, 4),
(9, NULL, 5);

CREATE TABLE `order_detail` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `order_id` INT(11) DEFAULT NULL,
  `product_id` INT(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

INSERT INTO `order_detail`(`id`, `order_id`, `product_id`) VALUES 
(1, 1, 1),
(2, 1, 2),
(3, 1, 3),
(4, 2, 3),
(5, 2, 1),
(6, 3, 2);

CREATE TABLE `product` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(25) DEFAULT NULL,
  `price` DOUBLE DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

INSERT INTO `product`(`id`, `name`, `price`) VALUES 
(1, 'Java教程', 128),
(2, 'C语言教程', 138),
(3, 'Python教程', 132.35);

image-20250227223837779

实体类:

package com.yimeng.domain;

import lombok.Data;
import java.util.List;

@Data
public class Order {
    private int id;
    private int orderNum;
    private List<Product> products;
}
package com.yimeng.domain;

import lombok.Data;
import java.util.List;

@Data
public class Product {
    private int id;
    private String name;
    private Double price;
    private List<Order> orders;
}
package com.yimeng.mapper;

import com.yimeng.domain.Order;

public interface OrderMapper {
    public Order selectOrdersAndProductsById(Long id);
}
<?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="com.yimeng.mapper.OrderMapper">

    <resultMap type="com.yimeng.domain.Order" id="orderMap">
        <id property="id" column="id" />
        <result property="orderNum" column="order_num" />
        <collection property="products" ofType="com.yimeng.domain.Product">
            <id property="id" column="product_id" />
            <result property="name" column="name" />
            <result property="price" column="price" />
        </collection>
    </resultMap>

    <select id="selectOrdersAndProductsById" resultMap="orderMap">
        SELECT
            o.id,
            o.order_num,
            p.id AS product_id,
            p.name,
            p.price
        FROM `order` o
            LEFT JOIN order_detail od  ON o.id = od.order_id
            LEFT JOIN product p ON p.id = od.product_id
        WHERE o.id = #{id}
    </select>
</mapper>
package com.yimeng.mapper;

import com.yimeng.domain.Product;

public interface ProductMapper {
    public Product selectProductAndOrdersById(Long id);
}
<?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="com.yimeng.mapper.ProductMapper">

    <resultMap type="com.yimeng.domain.Product" id="productMap">
        <id property="id" column="id" />
        <result property="name" column="name" />
        <result property="price" column="price" />
        <collection property="orders" ofType="com.yimeng.domain.Order">
            <id property="id" column="order_id" />
            <result property="orderNum" column="order_num" />
        </collection>
    </resultMap>

    <select id="selectProductAndOrdersById" resultMap="productMap">
        SELECT
            p.id,
            p.name,
            p.price,
            o.id AS order_id,
            o.order_num
        FROM product p
                 LEFT JOIN order_detail od ON p.id = od.product_id
                 LEFT JOIN `order` o ON o.id = od.order_id
        WHERE p.id = #{id}
    </select>
</mapper>

测试类及执行结果:

package com.yimeng;

import com.yimeng.domain.Order;
import com.yimeng.domain.Product;
import com.yimeng.mapper.OrderMapper;
import com.yimeng.mapper.ProductMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;

@SpringBootTest
public class SpringbootMybatisApplicationTests {

    @Resource
    private OrderMapper orderMapper;

    @Test
    public void test() {
        Order order = orderMapper.selectOrdersAndProductsById(1L);
        System.out.println(order);// Order(id=1, orderNum=20200107, products=[Product(id=1, name=Java教程, price=128.0, orders=null), Product(id=2, name=C语言教程, price=138.0, orders=null), Product(id=3, name=Python教程, price=132.35, orders=null)])
    }

    @Resource
    private ProductMapper productMapper;
    @Test
    public void testProductAndOrders() {
        Product product = productMapper.selectProductAndOrdersById(1L);
        System.out.println(product);// Product(id=1, name=Java教程, price=128.0, orders=[Order(id=1, orderNum=20200107, products=null), Order(id=2, orderNum=20200806, products=null)])
    }
}
综合的例子

这个例子包括一对一关系,多对多关系,并你可以看到更好的写法,就是association和collection中可以写resultMap,把内部的映射抽取出来。下面案例中只是保留了一些关键的内容,其他的都省略了。

package com.ruoyi.system.service.impl;

/**
 * 用户 业务层处理
 * 
 * @author ruoyi
 */
@Service
public class SysUserServiceImpl implements ISysUserService
{
    @Autowired
    private SysUserMapper userMapper;
   	/**
     * 通过用户ID查询用户
     * 
     * @param userId 用户ID
     * @return 用户对象信息
     */
     @Override
     public SysUser selectUserById(Long userId)
     {
         return userMapper.selectUserById(userId);
     }
}
package com.ruoyi.system.mapper;

import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.common.core.domain.entity.SysUser;

/**
 * 用户表 数据层
 * 
 * @author ruoyi
 */
public interface SysUserMapper
{
    /**
     * 通过用户ID查询用户
     * 
     * @param userId 用户ID
     * @return 用户对象信息
     */
    public SysUser selectUserById(Long userId);
}
<?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="com.ruoyi.system.mapper.SysUserMapper">

    <resultMap type="SysUser" id="SysUserResult">
        <id     property="userId"       column="user_id"      />
        <result property="deptId"       column="dept_id"      />
        <result property="userName"     column="user_name"    />
        <result property="nickName"     column="nick_name"    />
        <result property="email"        column="email"        />
        <result property="phonenumber"  column="phonenumber"  />
        <result property="sex"          column="sex"          />
        <result property="avatar"       column="avatar"       />
        <result property="password"     column="password"     />
        <result property="status"       column="status"       />
        <result property="delFlag"      column="del_flag"     />
        <result property="loginIp"      column="login_ip"     />
        <result property="loginDate"    column="login_date"   />
        <result property="createBy"     column="create_by"    />
        <result property="createTime"   column="create_time"  />
        <result property="updateBy"     column="update_by"    />
        <result property="updateTime"   column="update_time"  />
        <result property="remark"       column="remark"       />
        <association property="dept"    column="dept_id" javaType="SysDept" resultMap="deptResult" />
        <collection  property="roles"   javaType="java.util.List"           resultMap="RoleResult" />
    </resultMap>
	
    <resultMap id="deptResult" type="SysDept">
        <id     property="deptId"    column="dept_id"     />
        <result property="parentId"  column="parent_id"   />
        <result property="deptName"  column="dept_name"   />
        <result property="ancestors" column="ancestors"   />
        <result property="orderNum"  column="order_num"   />
        <result property="leader"    column="leader"      />
        <result property="status"    column="dept_status" />
    </resultMap>
	
    <resultMap id="RoleResult" type="SysRole">
        <id     property="roleId"       column="role_id"        />
        <result property="roleName"     column="role_name"      />
        <result property="roleKey"      column="role_key"       />
        <result property="roleSort"     column="role_sort"      />
        <result property="dataScope"     column="data_scope"    />
        <result property="status"       column="role_status"    />
    </resultMap>
	
	<sql id="selectUserVo">
        select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, 
        d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status,
        r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
        from sys_user u
		    left join sys_dept d on u.dept_id = d.dept_id
		    left join sys_user_role ur on u.user_id = ur.user_id
		    left join sys_role r on r.role_id = ur.role_id
    </sql>
	
	<select id="selectUserById" parameterType="Long" resultMap="SysUserResult">
		<include refid="selectUserVo"/>
		where u.user_id = #{userId}
	</select>
</mapper> 
package com.ruoyi.common.core.domain.entity;

import java.util.Date;
import java.util.List;
import javax.validation.constraints.*;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.annotation.Excel.ColumnType;
import com.ruoyi.common.annotation.Excel.Type;
import com.ruoyi.common.annotation.Excels;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.xss.Xss;

/**
 * 用户对象 sys_user
 * 
 * @author ruoyi
 */
@Data
public class SysUser extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 用户ID */
    @Excel(name = "用户序号", cellType = ColumnType.NUMERIC, prompt = "用户编号")
    private Long userId;

    /** 部门ID */
    @Excel(name = "部门编号", type = Type.IMPORT)
    private Long deptId;

    /** 用户账号 */
    @Excel(name = "登录名称")
    private String userName;

    /** 用户昵称 */
    @Excel(name = "用户名称")
    private String nickName;

    /** 用户邮箱 */
    @Excel(name = "用户邮箱")
    private String email;

    /** 手机号码 */
    @Excel(name = "手机号码")
    private String phonenumber;

    /** 用户性别 */
    @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知")
    private String sex;

    /** 用户头像 */
    private String avatar;

    /** 密码 */
    private String password;

    /** 帐号状态(0正常 1停用) */
    @Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用")
    private String status;

    /** 删除标志(0代表存在 2代表删除) */
    private String delFlag;

    /** 最后登录IP */
    @Excel(name = "最后登录IP", type = Type.EXPORT)
    private String loginIp;

    /** 最后登录时间 */
    @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT)
    private Date loginDate;

    /** 部门对象 */
    @Excels({
        @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT),
        @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT)
    })
    private SysDept dept;

    /** 角色对象 */
    private List<SysRole> roles;

    /** 角色组 */
    private Long[] roleIds;

    /** 岗位组 */
    private Long[] postIds;

    /** 角色ID */
    private Long roleId;
}
package com.ruoyi.common.core.domain.entity;

import java.util.ArrayList;
import java.util.List;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * 部门表 sys_dept
 * 
 * @author ruoyi
 */
@Data
public class SysDept extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 部门ID */
    private Long deptId;

    /** 父部门ID */
    private Long parentId;

    /** 祖级列表 */
    private String ancestors;

    /** 部门名称 */
    private String deptName;

    /** 显示顺序 */
    private Integer orderNum;

    /** 负责人 */
    private String leader;

    /** 联系电话 */
    private String phone;

    /** 邮箱 */
    private String email;

    /** 部门状态:0正常,1停用 */
    private String status;

    /** 删除标志(0代表存在 2代表删除) */
    private String delFlag;

    /** 父部门名称 */
    private String parentName;
    
    /** 子部门 */
    private List<SysDept> children = new ArrayList<SysDept>();
}
package com.ruoyi.common.core.domain.entity;

import java.util.Set;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.annotation.Excel.ColumnType;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * 角色表 sys_role
 * 
 * @author ruoyi
 */
@Data
public class SysRole extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 角色ID */
    @Excel(name = "角色序号", cellType = ColumnType.NUMERIC)
    private Long roleId;

    /** 角色名称 */
    @Excel(name = "角色名称")
    private String roleName;

    /** 角色权限 */
    @Excel(name = "角色权限")
    private String roleKey;

    /** 角色排序 */
    @Excel(name = "角色排序")
    private Integer roleSort;

    /** 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限) */
    @Excel(name = "数据范围", readConverterExp = "1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限,5=仅本人数据权限")
    private String dataScope;

    /** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */
    private boolean menuCheckStrictly;

    /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */
    private boolean deptCheckStrictly;

    /** 角色状态(0正常 1停用) */
    @Excel(name = "角色状态", readConverterExp = "0=正常,1=停用")
    private String status;

    /** 删除标志(0代表存在 2代表删除) */
    private String delFlag;

    /** 用户是否存在此角色标识 默认不存在 */
    private boolean flag = false;

    /** 菜单组 */
    private Long[] menuIds;

    /** 部门组(数据权限) */
    private Long[] deptIds;

    /** 角色菜单权限 */
    private Set<String> permissions;
}

ii、resultMap相关的属性和子元素

下面是resultMap的全部属性。

<resultMap id="" type="" autoMapping="" extends="">
    <constructor><!-- 类再实例化时用来注入结果到构造方法 -->
        <idArg column="" javaType="" jdbcType="" typeHandler="" name="" select="" resultMap=""/><!-- ID参数,结果为ID -->
        <arg column="" javaType="" jdbcType="" typeHandler="" name="" select="" resultMap=""/><!-- 注入到构造方法的一个普通结果 --> 
    </constructor>
    <id property="" column="" javaType="" jdbcType="" typeHandler=""/><!-- 用于表示哪个列是主键 -->
    <result property="" column="" javaType="" jdbcType="" typeHandler=""/><!-- 注入到字段或JavaBean属性的普通结果 -->
    <association property="" column="" javaType="" jdbcType="" typeHandler="" select="" resultMap="" columnPrefix="" notNullColumn="" autoMapping="" foreignColumn="" resultSet=""/><!-- 用于一对一关联 -->
    <collection property="" column="" javaType=""  resultMap="" select="" jdbcType="" typeHandler="" columnPrefix="" fetchType="" notNullColumn="" autoMapping="" foreignColumn="" resultSet="" ofType=""/><!-- 用于一对多、多对多关联 -->
    <discriminator javaType="" column="" jdbcType="" typeHandler=""><!-- 使用结果值来决定使用哪个结果映射 -->
        <case value="" resultMap="" resultType=""/><!-- 基于某些值的结果映射 -->
    	<case value="" resultMap="" resultType=""/>
    </discriminator>
</resultMap>

属性总结:

属性 说明
property Java 实体类的属性名。
column 数据库列名。
javaType 一个 Java 类的完全限定名,或一个类型别名。一般可以省略,因为 MyBatis 可以自己推断出来。
jdbcType jdbcType需要写JDBC类型。
typeHandler 自定义类型处理器。这里你指定的类型处理器会覆盖 MyBatis 默认的处理器。这个属性值是一个类型处理器实现类的全限定名,或者是类型别名。
select 嵌套查询语句 ID。
resultMap 嵌套 resultMap
columnPrefix 列名前缀,用于区分多个表中的相同列名。
notNullColumn 指定非空列,只有当这些列非空时才会创建关联对象或集合元素。
autoMapping 是否启用自动映射。如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。
foreignColumn 外键列名,用于匹配父对象的列。
resultSet 指定结果集名称(用于存储过程或复杂查询)。
ofType 集合中元素的 Java 类型。
fetchType 加载方式,可选值为 lazy(延迟加载)或 eager(立即加载)。resultMap指定fetchType属性后,这个resultMap将忽略全局配置参数 lazyLoadingEnabled,而使用这个resultMap中fetchType指定的值确定是否延迟加载。
name 构造方法参数的名称(从 MyBatis 3.4.3 开始支持)。
value 字段的值,用于匹配当前 case
resultType 匹配当前值时使用的 Java 类型。
resultMap 元素的属性
  • id:resultMap 的唯一标识。resultMap 元素是指定数据库查询结果列名和java实体类之间的映射关系的,如果某个地方想要使用这个resultMap的映射规则就需要指定这个resultMap的id。

  • type:映射的 Java 类型,通常是 POJO 的完全限定名或类型别名。

  • autoMapping:是否启用自动映射。该属性会覆盖全局的 autoMappingBehavior 配置。默认值为未设置(unset)。我认为最好还是不要靠自动映射,因为一些情况无法自动映射,我记不住哪些是无法自动映射的情况,所以最好还是全部把映射关系写下来比较好。

    autoMappingBehavior的全局配置是在mybatis的核心配置文件mybatis-config.xml(不一定叫这个名字)中配置的。注意,一般使用默认的就行,不要使用FULL。

    <?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>
        <!-- 日志实现 -->
        <settings>
            <setting name="logImpl" value="STDOUT_LOGGING"/>
            <!-- 驼峰命名 -->
            <setting name="mapUnderscoreToCamelCase" value="true" />
            <!-- 自动映射行为 -->
            <setting name="autoMappingBehavior" value="PARTIAL" />
    <!--        autoMappingBehavior 有以下三种可选值:-->
    <!--        NONE:禁用自动映射。MyBatis 不会自动将查询结果映射到 Java 对象的属性上,必须显式配置 <result> 或 <id> 等映射规则。-->
    <!--        PARTIAL(默认值):部分自动映射。MyBatis 会自动映射没有嵌套结果映射的简单属性,但不会自动映射嵌套的结果(如 <association> 或 <collection>)。-->
    <!--        FULL:完全自动映射。MyBatis 会尝试自动映射所有属性,包括嵌套的结果。-->
        </settings>
    </configuration>
    
  • extends:继承另一个 resultMap,可以复用已有的映射配置。和java的继承一样,你一个resultMap中的extends指定另一个resultMap的id,那么继承的resultMap会拥有被继承的resultMap的全部映射关系,继承的resultMap可以覆盖被继承的resultMap的映射,也可以声明新的映射。比如,id为productMap1的resultMap存在<result property="name" column="name" />,id为productMap2的result Map继承了productMap1,并且productMap2中写了<result property="name" column="price" />,那么如果你某个查询的resultMap指定productMap2,这个查询出来的结果中,列为price的数据会被赋值给name属性,不会把查询结果的name列赋值给name属性。

resultMap 的子元素
constructor元素

constructor适用于给没有无参构造方法,只有带参构造方法的实体类来配置映射关系。

mybatis默认是先使用无参构造方法创建实体类对象,然后进行set赋值的(如果实体类没有set方法,那么mybatis会使用反射给实体类赋值),赋值完成后就是完成了查询结果和实体类的绑定了。

如果你实体类没有无参构造方法,你想要他使用带参构造方法来创建对象并绑定查询的结果列到实体类中去,那么你可以使用constructor元素来完成这个需求。constructor元素中你可以使用子元素idArg或者arg把查询的结果绑定到实体类中去。注意:构造方法中的参数顺序和constructor元素中写的idArg或者arg顺序需要一致,这样mybatis才知道把查询结果的哪个列给到哪个第几个变量(idArg、arg没有指定构造方法参数名的属性,所以只能使用顺序来绑定了)。

constructor元素内有两个子元素:

  • <idArg>:用于映射主键字段到构造方法的参数。
    • 属性:column、javaType、jdbcType、typeHandler、name、select、resultMap。
  • <arg>:用于映射普通字段到构造方法的参数。
    • 属性:column、javaType、jdbcType、typeHandler、name、select、resultMap。

<idArg><arg>的区别主要就是,<idArg>用于主键字段,<arg> 用于非主键字段。注意:<idArg>用于非主键字段或者<arg> 用于主键字段其实也是可以的。技术上是没有问题的,但是为了代码语义清晰和框架优化,建议主键字段使用 <idArg>,非主键字段使用 <arg>。框架优化是指:MyBatis 可能会对主键字段进行特殊处理(如缓存优化),使用 <idArg> 可以确保这些优化能生效。

从 3.4.3 版本开始,你通过idArg和arg的name属性指定构造方法形参的具体参数名,这样就可以不按顺序写入 arg和id元素了。

比如有构造方法:

public class Product {
    ……
    public Product(Integer id, String name1, Double price1) {
    	System.out.println("执行");
    	this.id = id;
    	this.name = name1;
    	this.price = price1;
	}
}

下面这个resultMap也可以完成映射(顺序不对):

<?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="com.yimeng.mapper.ProductMapper">
	<resultMap type="com.yimeng.domain.Product" id="productMap">
        <constructor>
            <idArg column="id" javaType="int" jdbcType="INTEGER" name="id"/>
            <arg column="price" javaType="Double" jdbcType="DOUBLE" name="price1"/>
            <arg column="name" javaType="String" jdbcType="VARCHAR" name="name1"/>
        </constructor>
        <collection property="orders" ofType="com.yimeng.domain.Order">
            <id property="id" column="order_id"/>
            <result property="orderNum" column="order_num"/>
        </collection>
    </resultMap>
    <select id="selectProductAndOrdersById" resultMap="productMap">
        SELECT p.id,
               p.name,
               p.price,
               o.id AS order_id,
               o.order_num
        FROM product p
                 LEFT JOIN order_detail od ON p.id = od.product_id
                 LEFT JOIN `order` o ON o.id = od.order_id
        WHERE p.id = #{id}
    </select>
</mapper>

给了name属性后,就算没有按照构造方法参数的顺序来写constructor的idArg和arg也可以。

id元素和result元素

在 MyBatis 的 resultMap 中,<id><result> 是用于映射查询结果到 Java 对象的元素。它们的主要区别在于 <id> 用于标识主键字段,而 <result> 用于映射普通字段

<id><result> 子元素是一样的,都是:property、column、javaType、jdbcType、typeHandler。

<id><result> 子元素的关系就和<idArg><arg>属性的关系一样,虽然使用result也可以指定查询结果中的主键字段和实体类属性相互映射,但是使用id来指定查询结果中的主键和实体类的属性相互映射会得到mybatis的优化,所以性能会好一点。

注意:<id>元素是帮助 MyBatis 识别结果集中的唯一对象。如果没有 <id>,MyBatis 可能会错误地将多行数据映射为同一个对象,尤其是嵌套查询(Nested Queries)和集合映射(Collection Mapping)。所以,主键最好都是用id元素来映射。

association元素

用于处理一对一关联关系。

  • 属性:property、column、javaType、select、resultMap、jdbcType、typeHandler、columnPrefix、notNullColumn、autoMapping、foreignColumn、resultSet、fetchType。
collection元素

用于处理一对多或多对多关联关系。

  • 属性:property、column、javaType、ofType、select、resultMap、jdbcType、typeHandler、columnPrefix、fetchType、notNullColumn、autoMapping、foreignColumn、resultSet。

注意:其中ofType是用于指定集合中元素类型的。

discriminator元素

用于根据某个字段的值选择不同的映射规则。就像是java中的switch-case一样,

  • 属性:javaType、column、jdbcType、typeHandler。
  • <case>:根据column指定查询字段名的值来匹配value值对应的case。case的resultMap 或 resultType可以确定使用哪一个映射。
    • 属性:value、resultType、resultMap。
对于association、collection、discriminator特别说明

对于association、collection、discriminator这三个元素,这里来进行更详细的说明说明一下。

association 元素详解

association 用于数据库一对一关系。 假设我们有两个表:UserOrder,一个用户可以有多个订单。我们想要查询所有用户及其关联的订单。

CREATE TABLE USER (
    id INT PRIMARY KEY AUTO_INCREMENT,  -- 用户ID,主键,自增
    NAME VARCHAR(255) NOT NULL,         -- 用户姓名
    age INT                             -- 用户年龄
);

CREATE TABLE Card (
    id INT PRIMARY KEY AUTO_INCREMENT,  -- 卡ID,主键,自增
    NAME VARCHAR(255) NOT NULL,         -- 卡名称
    pass VARCHAR(255) NOT NULL,         -- 卡密码
    user_id INT UNIQUE                  -- 用户ID,唯一约束确保一对一关系
);

-- 插入用户数据
INSERT INTO USER (NAME, age) VALUES 
('Alice', 25),
('Bob', 30),
('Charlie', 22),
('Diana', 28);

-- 插入卡数据,确保 user_id 与 User 表中的 id 匹配
INSERT INTO Card (NAME, pass, user_id) VALUES 
('Gold Card', 'alice123', 1),  -- Alice 的卡
('Silver Card', 'bob456', 2),   -- Bob 的卡
('Platinum Card', 'charlie789', 3),  -- Charlie 的卡
('VIP Card', 'diana101', 4);    -- Diana 的卡

image-20250303215336447

// Card 类
@Data
public class Card {
    private int id;
    private String name;
    private String pass;
    // getter 和 setter 省略
}

// User 类
@Data
public class User {
    private int id;
    private String name;
    private int age;
    private Card card; // 一对一关系,一个用户对应一张卡
    // getter 和 setter 省略
}
package com.yimeng.mapper;

import com.yimeng.domain.Card;
import org.apache.ibatis.annotations.Param;

public interface CardMapper  {
    /**
     * 根据用户ID查询卡信息
     */
    Card selectCard(@Param("userId") int userId);
}
<?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="com.yimeng.mapper.CardMapper">
    <!-- 定义 selectCard 的结果集 -->
    <resultMap id="cardResult" type="com.yimeng.domain.Card">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>
        <result column="pass" property="pass"></result>
    </resultMap>

    <!-- 查询卡信息 -->
    <select id="selectCard" resultMap="cardResult">
        SELECT * FROM card WHERE user_id = #{userId}
    </select>
</mapper>
package com.yimeng.mapper;

import com.yimeng.domain.User;
import java.util.List;

public interface UserMapper  {
    /**
     * 查询所有用户及其关联的卡信息
     */
    List<User> selectAllUsers();
}
<?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="com.yimeng.mapper.UserMapper">
    <!-- 定义 selectUser 的结果集 -->
    <resultMap id="userResult" type="com.yimeng.domain.User">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>
        <result column="age" property="age"></result>
        <!-- 使用 association 实现一对一关系 -->
        <association property="card" column="id" javaType="com.yimeng.domain.Card" select="com.yimeng.mapper.CardMapper.selectCard"/>
    </resultMap>

    <!-- 查询所有用户 -->
    <select id="selectAllUsers" resultMap="userResult">
        SELECT * FROM user
    </select>
</mapper>
package com.yimeng;

import com.yimeng.domain.User;
import com.yimeng.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;

@SpringBootTest
public class SpringbootMybatisApplicationTests {

    @Resource
    private UserMapper userMapper;

    @Test
    public void test() {
        List<User> list = userMapper.selectAllUsers();
        System.out.println(list);// [User(id=1, name=Alice, age=25, card=Card(id=1, name=Gold Card, pass=alice123)), User(id=2, name=Bob, age=30, card=Card(id=2, name=Silver Card, pass=bob456)), User(id=3, name=Charlie, age=22, card=Card(id=3, name=Platinum Card, pass=charlie789)), User(id=4, name=Diana, age=28, card=Card(id=4, name=VIP Card, pass=diana101))]
    }

}

上述代码的执行流程:

  1. 调用selectAllUsers语句进行查询,然后查询结果进入userResult结果集进行处理
  2. 在结果集出来过程中,结果集会去执行association的select属性所指的selectCard 语句,并且在执行过程中将 column 属性指定的 id 列的值作为参数传递过去。
  3. selectCard 语句中会把查询到的结果映射到 Card 对象中,然后将结果返回给userResult结果集,并注入给 User 对象的 card 属性。

注意:

这种方式虽然很简单,但在大型数据集或大型数据表上表现不佳。这个问题被称为“N+1 查询问题”。

N+1 查询问题是指:

  1. 执行 1 次主查询,获取一组主对象(例如 User)。
  2. 对于每个主对象,执行 N 次关联查询,加载关联的对象(例如 Card)。

这样,总的查询次数就是 1(主查询) + N(关联查询),这就是所谓的 N+1 查询问题。当 N 很大时,性能会显著下降。

N+1 查询问题的体现

假设数据库中有 100 个用户,每个用户对应一张卡。执行流程如下:

  1. 主查询
    • 执行 SELECT * FROM USER,查询所有用户(1 次查询)。
    • 返回 100 个用户。
  2. 关联查询
    • 对于每个用户,MyBatis 会执行 SELECT * FROM CARD WHERE user_id = ?,查询对应的卡信息。
    • 总共执行 100 次查询。
  3. 总查询次数
    • 1 次主查询 + 100 次关联查询 = 101 次查询

这就是典型的 N+1 查询问题。当用户数量增加时,查询次数会线性增长,导致性能显著下降。

如何解决 N+1 查询问题?

方法 1:使用 JOIN 查询嵌套结果映射

通过一次性查询所有数据(使用 JOIN),然后在结果映射中手动映射关联对象,可以避免 N+1 查询问题。

比如:

<?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="com.yimeng.mapper.UserMapper">
<!--    &lt;!&ndash; 定义 selectUser 的结果集 &ndash;&gt;-->
<!--    <resultMap id="userResult" type="com.yimeng.domain.User">-->
<!--        <id column="id" property="id"></id>-->
<!--        <result column="name" property="name"></result>-->
<!--        <result column="age" property="age"></result>-->
<!--        &lt;!&ndash; 使用 association 实现一对一关系 &ndash;&gt;-->
<!--        <association property="card" column="id" javaType="com.yimeng.domain.Card"-->
<!--                     select="com.yimeng.mapper.CardMapper.selectCard"/>-->
<!--    </resultMap>-->

<!--    &lt;!&ndash; 查询所有用户 &ndash;&gt;-->
<!--    <select id="selectAllUsers" resultMap="userResult">-->
<!--        SELECT * FROM user-->
<!--    </select>-->

    <!--JOIN 查询-->
    <resultMap id="userResultWithCard" type="com.yimeng.domain.User">
        <id column="user_id" property="id"></id>
        <result column="user_name" property="name"></result>
        <result column="user_age" property="age"></result>
        <!-- 使用 association 实现一对一关系 -->
        <association property="card" javaType="com.yimeng.domain.Card">
            <id column="card_id" property="id"></id>
            <result column="card_name" property="name"></result>
            <result column="card_pass" property="pass"></result>
        </association>
    </resultMap>

    <select id="selectAllUsers" resultMap="userResultWithCard">
        SELECT u.id   AS user_id,
               u.name AS user_name,
               u.age  AS user_age,
               c.id   AS card_id,
               c.name AS card_name,
               c.pass AS card_pass
        FROM USER u
                 LEFT JOIN CARD c ON u.id = c.user_id
    </select>
</mapper>

这个association中写的映射关系可以抽取出来,抽取出来的写法如下:

<?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="com.yimeng.mapper.UserMapper">
<!--    &lt;!&ndash; 定义 selectUser 的结果集 &ndash;&gt;-->
<!--    <resultMap id="userResult" type="com.yimeng.domain.User">-->
<!--        <id column="id" property="id"></id>-->
<!--        <result column="name" property="name"></result>-->
<!--        <result column="age" property="age"></result>-->
<!--        &lt;!&ndash; 使用 association 实现一对一关系 &ndash;&gt;-->
<!--        <association property="card" column="id" javaType="com.yimeng.domain.Card"-->
<!--                     select="com.yimeng.mapper.CardMapper.selectCard"/>-->
<!--    </resultMap>-->

<!--    &lt;!&ndash; 查询所有用户 &ndash;&gt;-->
<!--    <select id="selectAllUsers" resultMap="userResult">-->
<!--        SELECT * FROM user-->
<!--    </select>-->

<!--    &lt;!&ndash;JOIN 查询&ndash;&gt;-->
<!--    <resultMap id="userResultWithCard" type="com.yimeng.domain.User">-->
<!--        <id column="user_id" property="id"></id>-->
<!--        <result column="user_name" property="name"></result>-->
<!--        <result column="user_age" property="age"></result>-->
<!--        &lt;!&ndash; 使用 association 实现一对一关系 &ndash;&gt;-->
<!--        <association property="card" javaType="com.yimeng.domain.Card">-->
<!--            <id column="card_id" property="id"></id>-->
<!--            <result column="card_name" property="name"></result>-->
<!--            <result column="card_pass" property="pass"></result>-->
<!--        </association>-->
<!--    </resultMap>-->

<!--    <select id="selectAllUsers" resultMap="userResultWithCard">-->
<!--        SELECT u.id   AS user_id,-->
<!--               u.name AS user_name,-->
<!--               u.age  AS user_age,-->
<!--               c.id   AS card_id,-->
<!--               c.name AS card_name,-->
<!--               c.pass AS card_pass-->
<!--        FROM USER u-->
<!--                 LEFT JOIN CARD c ON u.id = c.user_id-->
<!--    </select>-->

    <!-- 定义 Card 的结果映射 -->
    <resultMap id="cardResult" type="com.yimeng.domain.Card">
        <id column="card_id" property="id"></id>
        <result column="card_name" property="name"></result>
        <result column="card_pass" property="pass"></result>
    </resultMap>

    <!-- 定义 User 的结果映射 -->
    <resultMap id="userResultWithCard" type="com.yimeng.domain.User">
        <id column="user_id" property="id"></id>
        <result column="user_name" property="name"></result>
        <result column="user_age" property="age"></result>
        <!-- 使用 association 引用 cardResult -->
        <association property="card" resultMap="cardResult"/>
    </resultMap>

    <!-- 查询所有用户及其关联的卡信息 -->
    <select id="selectAllUsers" resultMap="userResultWithCard">
        SELECT u.id AS user_id, u.name AS user_name, u.age AS user_age,
               c.id AS card_id, c.name AS card_name, c.pass AS card_pass
        FROM USER u
                 LEFT JOIN CARD c ON u.id = c.user_id
    </select>
</mapper>

这样写的好处是:

  1. Card 的结果映射提取出来后,可以在其他地方复用 cardResult,避免重复定义。
  2. 提取后的代码结构更清晰,主 <resultMap> 只关注 User 的映射,Card 的映射单独定义。
  3. 如果需要修改 Card 的映射逻辑,只需修改 cardResult,而不需要修改多个地方。其他地方只是引用了这里的映射逻辑,所以源头改了所有都会改。

columnPrefix的使用:

通过使用 columnPrefix,我们可以清晰地处理 JOIN 查询中列名的前缀问题,避免列名冲突,并正确地将带有前缀的列映射到对象的属性中。

<?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="com.yimeng.mapper.UserMapper">
<!--    &lt;!&ndash; 定义 selectUser 的结果集 &ndash;&gt;-->
<!--    <resultMap id="userResult" type="com.yimeng.domain.User">-->
<!--        <id column="id" property="id"></id>-->
<!--        <result column="name" property="name"></result>-->
<!--        <result column="age" property="age"></result>-->
<!--        &lt;!&ndash; 使用 association 实现一对一关系 &ndash;&gt;-->
<!--        <association property="card" column="id" javaType="com.yimeng.domain.Card"-->
<!--                     select="com.yimeng.mapper.CardMapper.selectCard"/>-->
<!--    </resultMap>-->

<!--    &lt;!&ndash; 查询所有用户 &ndash;&gt;-->
<!--    <select id="selectAllUsers" resultMap="userResult">-->
<!--        SELECT * FROM user-->
<!--    </select>-->

<!--    &lt;!&ndash;JOIN 查询&ndash;&gt;-->
<!--    <resultMap id="userResultWithCard" type="com.yimeng.domain.User">-->
<!--        <id column="user_id" property="id"></id>-->
<!--        <result column="user_name" property="name"></result>-->
<!--        <result column="user_age" property="age"></result>-->
<!--        &lt;!&ndash; 使用 association 实现一对一关系 &ndash;&gt;-->
<!--        <association property="card" javaType="com.yimeng.domain.Card">-->
<!--            <id column="card_id" property="id"></id>-->
<!--            <result column="card_name" property="name"></result>-->
<!--            <result column="card_pass" property="pass"></result>-->
<!--        </association>-->
<!--    </resultMap>-->

<!--    <select id="selectAllUsers" resultMap="userResultWithCard">-->
<!--        SELECT u.id   AS user_id,-->
<!--               u.name AS user_name,-->
<!--               u.age  AS user_age,-->
<!--               c.id   AS card_id,-->
<!--               c.name AS card_name,-->
<!--               c.pass AS card_pass-->
<!--        FROM USER u-->
<!--                 LEFT JOIN CARD c ON u.id = c.user_id-->
<!--    </select>-->

<!--    &lt;!&ndash; 定义 Card 的结果映射 &ndash;&gt;-->
<!--    <resultMap id="cardResult" type="com.yimeng.domain.Card">-->
<!--        <id column="card_id" property="id"></id>-->
<!--        <result column="card_name" property="name"></result>-->
<!--        <result column="card_pass" property="pass"></result>-->
<!--    </resultMap>-->

<!--    &lt;!&ndash; 定义 User 的结果映射 &ndash;&gt;-->
<!--    <resultMap id="userResultWithCard" type="com.yimeng.domain.User">-->
<!--        <id column="user_id" property="id"></id>-->
<!--        <result column="user_name" property="name"></result>-->
<!--        <result column="user_age" property="age"></result>-->
<!--        &lt;!&ndash; 使用 association 引用 cardResult &ndash;&gt;-->
<!--        <association property="card" resultMap="cardResult"/>-->
<!--    </resultMap>-->

<!--    &lt;!&ndash; 查询所有用户及其关联的卡信息 &ndash;&gt;-->
<!--    <select id="selectAllUsers" resultMap="userResultWithCard">-->
<!--        SELECT u.id AS user_id, u.name AS user_name, u.age AS user_age,-->
<!--               c.id AS card_id, c.name AS card_name, c.pass AS card_pass-->
<!--        FROM USER u-->
<!--                 LEFT JOIN CARD c ON u.id = c.user_id-->
<!--    </select>-->

    <!-- 定义 Card 的结果映射 -->
    <resultMap id="cardResult" type="com.yimeng.domain.Card">
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="pass" property="pass"/>
    </resultMap>

    <!-- 定义 User 的结果映射 -->
    <resultMap id="userResultWithCard" type="com.yimeng.domain.User">
        <id column="user_id" property="id"></id>
        <result column="user_name" property="name"></result>
        <result column="user_age" property="age"></result>
        <!-- 使用 association 引用 cardResult -->
        <association property="card" resultMap="cardResult" columnPrefix="card_"/>
    </resultMap>

    <!-- 查询所有用户及其关联的卡信息 -->
    <select id="selectAllUsers" resultMap="userResultWithCard">
        SELECT u.id AS user_id, u.name AS user_name, u.age AS user_age,
               c.id AS card_id, c.name AS card_name, c.pass AS card_pass
        FROM USER u
                 LEFT JOIN CARD c ON u.id = c.user_id
    </select>
</mapper>

方法 2:使用 延迟加载(Lazy Loading)

MyBatis 支持延迟加载,即只有在访问关联对象时才会执行查询。这样可以避免一次性加载所有关联数据。

做法如下:

  1. 在 MyBatis 配置文件中启用延迟加载:

    <?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>
        <!-- 日志实现 -->
        <settings>
            <setting name="logImpl" value="STDOUT_LOGGING"/>
            <!-- 驼峰命名 -->
            <setting name="mapUnderscoreToCamelCase" value="true" />
            <!-- 启用延迟加载 -->
            <setting name="lazyLoadingEnabled" value="true"/>
            <!-- 禁用积极加载(按需加载) -->
            <setting name="aggressiveLazyLoading" value="false"/>
        </settings>
    </configuration>
    

    根据 MyBatis 官方文档(截至最新版本, 3.5.x),以下是这两个配置项的默认值:

    1. lazyLoadingEnabled:

      • 默认值:false(关闭)
      • 含义:控制是否启用延迟加载。如果设置为 false,关联对象会在主查询执行时立即加载(即急切加载);如果设置为 true,关联对象会在首次访问时才加载(即延迟加载)。resultMap指定fetchType属性后,这个resultMap将忽略全局配置参数 lazyLoadingEnabled,而使用这个resultMap中fetchType指定的值确定是否延迟加载。如果resultMap中没有指定fetchType,那么这个resultMap会看全局配置文件中的lazyLoadingEnabled,确定是否进行延迟加载。
    2. aggressiveLazyLoading:

      • 默认值:false(关闭)(在 MyBatis 3.4.1 及以上版本)

      • 含义:控制延迟加载的触发方式。如果设置为 true,任何方法调用都会触发所有延迟加载属性的加载(积极加载);如果设置为 false,只有在访问具体延迟加载的属性时才会触发加载(按需加载)。

      • 历史背景:在 MyBatis 3.4.0 之前的版本中,aggressiveLazyLoading 的默认值是 true,但从 3.4.1 开始改为 false,以更符合现代开发需求。

  2. 修改映射文件

    在映射文件中,使用 <association>fetchType="lazy" 属性来指定延迟加载。

    <?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="com.yimeng.mapper.CardMapper">
        <!-- 定义 selectCard 的结果集 -->
        <resultMap id="cardResult" type="com.yimeng.domain.Card">
            <id column="card_id" property="id"></id>
            <result column="card_name" property="name"></result>
            <result column="card_pass" property="pass"></result>
        </resultMap>
    
        <!-- 查询卡信息 -->
        <select id="selectCard" resultMap="cardResult">
            SELECT c.id AS card_id, c.name AS card_name, c.pass AS card_pass
            FROM card c
            WHERE c.user_id = #{userId}
        </select>
    </mapper>
    
    <?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="com.yimeng.mapper.UserMapper">
        
        <!-- 定义 User 的结果映射 -->
        <resultMap id="userResultWithCard" type="com.yimeng.domain.User">
            <id column="user_id" property="id"></id>
            <result column="user_name" property="name"></result>
            <result column="user_age" property="age"></result>
            <!-- 使用 association 实现一对一关系,并启用延迟加载 -->
            <association property="card" column="user_id" javaType="com.yimeng.domain.Card" fetchType="lazy" select="com.yimeng.mapper.CardMapper.selectCard"/>
        </resultMap>
    
        <!-- 查询所有用户 -->
        <select id="selectAllUsers" resultMap="userResultWithCard">
            SELECT u.id AS user_id, u.name AS user_name, u.age AS user_age
            FROM user u
        </select>
    </mapper>
    
  3. 完整代码:

    package com.yimeng.domain;
    
    import lombok.Data;
    
    // Card 类
    @Data
    public class Card {
        private int id;
        private String name;
        private String pass;
        // getter 和 setter 省略
    }
    
    package com.yimeng.domain;
    
    import lombok.Data;
    
    // User 类
    @Data
    public class User {
        private int id;
        private String name;
        private int age;
        private Card card; // 一对一关系,一个用户对应一张卡
        // getter 和 setter 省略
    }
    
    package com.yimeng.mapper;
    
    import com.yimeng.domain.Card;
    import org.apache.ibatis.annotations.Param;
    
    public interface CardMapper  {
        /**
         * 根据用户ID查询卡信息
         */
        Card selectCard(@Param("userId") int userId);
    }
    
    package com.yimeng.mapper;
    
    import com.yimeng.domain.User;
    import java.util.List;
    
    public interface UserMapper  {
        /**
         * 查询所有用户及其关联的卡信息
         */
        List<User> selectAllUsers();
    }
    
    package com.yimeng.service.impl;
    
    import com.yimeng.domain.User;
    import com.yimeng.mapper.UserMapper;
    import com.yimeng.service.UserService;
    import org.springframework.stereotype.Service;
    import javax.annotation.Resource;
    import java.util.List;
    
    @Service
    public class UserServiceImpl implements UserService {
        @Resource
        private UserMapper userMapper;
    
        @Override
        public void selectAllUsers() {
            // 执行List<User> list = userMapper.selectAllUsers();的时候,建立了连接,并且去执行了SELECT u.id AS user_id, u.name AS user_name, u.age AS user_age FROM user u
            List<User> list = userMapper.selectAllUsers();
            // 执行System.out.println("查询全部数据:"+list);的时候,建立了连接,并且去执行了SELECT c.id AS card_id, c.name AS card_name, c.pass AS card_pass FROM card c WHERE c.user_id = ?
            System.out.println("查询全部数据:"+list);// 查询全部数据:[User(id=1, name=Alice, age=25, card=Card(id=1, name=Gold Card, pass=alice123)), User(id=2, name=Bob, age=30, card=Card(id=2, name=Silver Card, pass=bob456)), User(id=3, name=Charlie, age=22, card=Card(id=3, name=Platinum Card, pass=charlie789)), User(id=4, name=Diana, age=28, card=Card(id=4, name=VIP Card, pass=diana101))]
    
            for (int i = 0; i < list.size(); i++) {
                System.out.println("查询单个数据:"+list.get(i).getCard());// 这里不是走了缓存,是因为上面执行System.out.println("查询全部数据:"+list);的时候已经给card属性赋值了。
            }
        }
    }
    
    package com.yimeng.service;
    
    public interface UserService {
        void selectAllUsers();
    }
    
    <?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="com.yimeng.mapper.CardMapper">
        <!-- 定义 selectCard 的结果集 -->
        <resultMap id="cardResult" type="com.yimeng.domain.Card">
            <id column="card_id" property="id"></id>
            <result column="card_name" property="name"></result>
            <result column="card_pass" property="pass"></result>
        </resultMap>
    
        <!-- 查询卡信息 -->
        <select id="selectCard" resultMap="cardResult">
            SELECT c.id AS card_id, c.name AS card_name, c.pass AS card_pass
            FROM card c
            WHERE c.user_id = #{userId}
        </select>
    </mapper>
    
    <?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="com.yimeng.mapper.UserMapper">
    
        <!-- 定义 User 的结果映射 -->
        <resultMap id="userResultWithCard" type="com.yimeng.domain.User">
            <id column="user_id" property="id"></id>
            <result column="user_name" property="name"></result>
            <result column="user_age" property="age"></result>
            <!-- 使用 association 实现一对一关系,并启用延迟加载 -->
            <association property="card" column="user_id" javaType="com.yimeng.domain.Card" fetchType="lazy" select="com.yimeng.mapper.CardMapper.selectCard"/>
        </resultMap>
    
        <!-- 查询所有用户 -->
        <select id="selectAllUsers" resultMap="userResultWithCard">
            SELECT u.id AS user_id, u.name AS user_name, u.age AS user_age
            FROM user u
        </select>
    </mapper>
    
    <?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>
        <!-- 日志实现 -->
        <settings>
            <setting name="logImpl" value="STDOUT_LOGGING"/>
            <!-- 驼峰命名 -->
            <setting name="mapUnderscoreToCamelCase" value="true" />
            <!-- 启用延迟加载 -->
            <setting name="lazyLoadingEnabled" value="true"/>
            <!-- 禁用积极加载(按需加载) -->
            <setting name="aggressiveLazyLoading" value="false"/>
        </settings>
    </configuration>
    
    package com.yimeng;
    
    import com.yimeng.domain.User;
    import com.yimeng.mapper.UserMapper;
    import com.yimeng.service.UserService;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    import javax.annotation.Resource;
    import java.util.List;
    
    @SpringBootTest
    public class SpringbootMybatisApplicationTests {
    
        @Resource
        private UserService userService;
    
        @Test
        public void test() {
            userService.selectAllUsers();
        }
    }
    

延迟加载的执行流程

  1. 调用 selectAllUsers
    • 执行 SELECT u.id AS user_id, u.name AS user_name, u.age AS user_age FROM USER u,查询所有用户。
    • 返回的 User 对象中,card 属性是延迟加载的代理对象。
  2. 访问 card 属性
    • 当代码中首次访问 user.getCard() 时,MyBatis 会触发 selectCardByUserId 查询。
    • 执行 SELECT c.id AS card_id, c.name AS card_name, c.pass AS card_pass FROM CARD c WHERE c.user_id = ?,查询对应的卡信息。
    • 将查询结果映射到 Card 对象,并注入到 User 对象的 card 属性中。
  3. 后续访问
    • 如果再次访问 user.getCard(),MyBatis 会直接返回已加载的 Card 对象,而不会再次查询数据库。

延迟加载的优点

  1. 减少初始查询的开销
    • 只有在访问关联对象时才会执行查询,减少了初始查询的负载。
  2. 按需加载
    • 适合关联对象不经常使用的场景,避免加载不必要的数据。
  3. 提升性能
    • 在数据量较大或关联对象较多时,延迟加载可以显著提升性能。

延迟加载的缺点

  1. N+1 查询问题
    • 如果查询多个主对象,并且每个主对象都访问关联对象,仍然会导致 N+1 查询问题。
    • 例如,查询 100 个用户,每个用户都访问 card 属性,会导致 100 次额外的查询。如果你查询了 100 个用户,但只有 20 个用户访问了 card 属性,那么只会触发 20 次额外的查询,而不是 100 次。即,只有实际访问了 card 属性的用户才会触发额外的查询。所以,如果你查询的用户里面,只有少部分要去看card,那么还是适合使用的。
  2. 调试困难
    • 延迟加载的行为是隐式的,只有在访问关联对象时才会触发查询,可能导致调试和维护困难。

所以,建议还是使用一次查询来处理多表查询的情况,不要使用分步查询,就算是分步查询使用了延迟加载,依然还是可能存在一定的问题。

collection 元素详解

collection 只是比association 新增的 “ofType” 属性,其他属性和 association 完全一样。所以那些属性也就不用介绍了。

ofType这个属性非常重要,它用于指定集合中元素的类型。

关于collection的使用例子,在前面已经讲过了。不管是单步查询还是分步查询的例子前面都讲过(看“4、resultMap——>i、resultMap标签的使用——>select标签配合resultMap标签完成复杂映射——>一对多”)。

discriminator 元素详解(鉴别器)

有时候,一个数据库查询可能会返回多个不同的结果集(但总体上还是有一定的联系的)。 鉴别器(discriminator)元素就是被设计来应对这种情况的,另外也能处理其它情况,例如类的继承层次结构。 鉴别器的概念很好理解——它很像 Java 语言中的 switch 语句。

一个鉴别器的定义需要指定 column 和 javaType 属性。column 指定了 MyBatis 查询被比较值的是哪个字段。 而 javaType 是被比较值的字段对应的java类型。

例子:

CREATE TABLE vehicle (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    TYPE VARCHAR(50) NOT NULL COMMENT '车辆类型(car 或 truck)',
    brand VARCHAR(50) NOT NULL COMMENT '品牌',
    model VARCHAR(50) NOT NULL COMMENT '型号',
    seating_capacity INT COMMENT 'car 特有的字段(座位数)',
    load_capacity DECIMAL(10, 2) COMMENT 'truck 特有的字段(载重容量)'
) COMMENT '车辆信息表';

INSERT INTO vehicle (TYPE, brand, model, seating_capacity, load_capacity) VALUES
('car', 'Toyota', 'Camry', 5, NULL),
('truck', 'Ford', 'F-150', NULL, 2000.50),
('car', 'Honda', 'Civic', 4, NULL);

image-20250304230331400

package com.yimeng.domain;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

/**
 * 轿车类
 */
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Car extends Vehicle {
    private Integer seatingCapacity; // 座位数
}
package com.yimeng.domain;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

/**
 * 卡车类
 */
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Truck extends Vehicle {
    private Double loadCapacity; // 载重容量
}
package com.yimeng.domain;

import lombok.Data;

/**
 * 车辆基类
 */
@Data
public abstract class Vehicle {
    private Integer id;          // 主键ID
    private String type;         // 车辆类型
    private String brand;        // 品牌
    private String model;        // 型号
}
package com.yimeng.mapper;

import com.yimeng.domain.Vehicle;
import org.apache.ibatis.annotations.Param;

/**
 * 车辆信息 Mapper 接口
 */
public interface VehicleMapper {

    /**
     * 根据 ID 查询车辆信息
     *
     * @param id 车辆ID
     * @return 车辆信息
     */
    Vehicle selectVehicleById(@Param("id") Integer id);

    /**
     * 插入车辆信息
     *
     * @param vehicle 车辆信息
     * @return 受影响的行数
     */
    int insertVehicle(@Param("vehicle") Vehicle vehicle);
}
<?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="com.yimeng.mapper.VehicleMapper">
    <!-- 基础 resultMap -->
    <resultMap id="VehicleResultMap" type="Vehicle">
        <id property="id" column="id"/>
        <result property="type" column="type"/>
        <result property="brand" column="brand"/>
        <result property="model" column="model"/>

        <!-- discriminator 根据 type 字段选择不同的 resultMap -->
        <discriminator javaType="String" column="type">
            <case value="car" resultMap="CarResultMap"/>
            <case value="truck" resultMap="TruckResultMap"/>
        </discriminator>
    </resultMap>

    <!-- Car 的 resultMap -->
    <resultMap id="CarResultMap" type="Car" extends="VehicleResultMap">
        <result property="seatingCapacity" column="seating_capacity"/>
    </resultMap>

    <!-- Truck 的 resultMap -->
    <resultMap id="TruckResultMap" type="Truck" extends="VehicleResultMap">
        <result property="loadCapacity" column="load_capacity"/>
    </resultMap>

    <!-- 根据 ID 查询车辆信息 -->
    <select id="selectVehicleById" resultMap="VehicleResultMap">
        SELECT id, type, brand, model, seating_capacity, load_capacity
        FROM vehicle
        WHERE id = #{id}
    </select>

    <!-- 插入车辆信息 -->
    <insert id="insertVehicle" parameterType="Vehicle">
        INSERT INTO vehicle (type, brand, model, seating_capacity, load_capacity)
        VALUES (
        #{vehicle.type},
        #{vehicle.brand},
        #{vehicle.model},
        <choose>
            <when test="vehicle.type == 'car'">#{vehicle.seatingCapacity}, NULL</when>
            <when test="vehicle.type == 'truck'">NULL, #{vehicle.loadCapacity}</when>
            <otherwise>NULL, NULL</otherwise>
        </choose>
        )
    </insert>
</mapper>

如果你不喜欢外部定义映射,你也可以这样写(resultType是自动映射的,但是要求字段名要匹配):

<?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="com.yimeng.mapper.VehicleMapper">
    <!-- 基础 resultMap -->
    <resultMap id="VehicleResultMap" type="Vehicle">
        <id property="id" column="id"/>
        <result property="type" column="type"/>
        <result property="brand" column="brand"/>
        <result property="model" column="model"/>

<!--        &lt;!&ndash; discriminator 根据 type 字段选择不同的 resultMap &ndash;&gt;-->
<!--        <discriminator javaType="String" column="type">-->
<!--            <case value="car" resultMap="CarResultMap"/>-->
<!--            <case value="truck" resultMap="TruckResultMap"/>-->
<!--        </discriminator>-->

        <discriminator javaType="String" column="type">
            <case value="car" resultType="com.yimeng.domain.Car"></case>
            <case value="truck" resultType="com.yimeng.domain.Truck"></case>
        </discriminator>
    </resultMap>

    <!-- Car 的 resultMap -->
    <resultMap id="CarResultMap" type="Car" extends="VehicleResultMap">
        <result property="seatingCapacity" column="seating_capacity"/>
    </resultMap>

    <!-- Truck 的 resultMap -->
    <resultMap id="TruckResultMap" type="Truck" extends="VehicleResultMap">
        <result property="loadCapacity" column="load_capacity"/>
    </resultMap>

    <!-- 根据 ID 查询车辆信息 -->
    <select id="selectVehicleById" resultMap="VehicleResultMap">
        SELECT id, type, brand, model, seating_capacity, load_capacity
        FROM vehicle
        WHERE id = #{id}
    </select>

    <!-- 插入车辆信息 -->
    <insert id="insertVehicle" parameterType="Vehicle">
        INSERT INTO vehicle (type, brand, model, seating_capacity, load_capacity)
        VALUES (
        #{vehicle.type},
        #{vehicle.brand},
        #{vehicle.model},
        <choose>
            <when test="vehicle.type == 'car'">#{vehicle.seatingCapacity}, NULL</when>
            <when test="vehicle.type == 'truck'">NULL, #{vehicle.loadCapacity}</when>
            <otherwise>NULL, NULL</otherwise>
        </choose>
        )
    </insert>
</mapper>

测试类:

package com.yimeng;

import com.yimeng.domain.Car;
import com.yimeng.domain.Truck;
import com.yimeng.domain.Vehicle;
import com.yimeng.mapper.VehicleMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
public class SpringbootMybatisApplicationTests {

    @Resource
    private VehicleMapper vehicleMapper;

    @Test
    public void testSelectVehicleById() {
        // 查询车辆信息
        Vehicle vehicle = vehicleMapper.selectVehicleById(1);
        assertNotNull(vehicle);
        System.out.println("查询到的车辆信息:" + vehicle);

        if (vehicle instanceof Car) {
            Car car = (Car) vehicle;
            System.out.println("这是一辆轿车,座位数:" + car.getSeatingCapacity());
        } else if (vehicle instanceof Truck) {
            Truck truck = (Truck) vehicle;
            System.out.println("这是一辆卡车,载重容量:" + truck.getLoadCapacity());
        }
        /*  输出结果:
            查询到的车辆信息:Car(super=Vehicle(id=1, type=car, brand=Toyota, model=Camry), seatingCapacity=5)
            这是一辆轿车,座位数:5
         */
    }

    @Test
    public void testInsertCar() {
        // 创建一辆轿车
        Car car = new Car();
        car.setType("car");
        car.setBrand("Tesla");
        car.setModel("Model S");
        car.setSeatingCapacity(5);

        // 插入轿车信息
        int result = vehicleMapper.insertVehicle(car);
        assertEquals(1, result);
        System.out.println("插入轿车成功,受影响的行数:" + result);
    }

    @Test
    public void testInsertTruck() {
        // 创建一辆卡车
        Truck truck = new Truck();
        truck.setType("truck");
        truck.setBrand("Ford");
        truck.setModel("F-150");
        truck.setLoadCapacity(2000.50);

        // 插入卡车信息
        int result = vehicleMapper.insertVehicle(truck);
        assertEquals(1, result);
        System.out.println("插入卡车成功,受影响的行数:" + result);
    }
}

在这个示例中,MyBatis 会从结果集中得到每条记录,然后比较它的 type 值。 如果它匹配任意一个鉴别器的 case,就会使用这个 case 指定的结果映射。 这个过程是互斥的,也就是说,剩余的结果映射将被忽略。

注意点

特殊字符转义

在解析XML文件时将5种特殊字符进行转义,分别是&, <, >, ",’, 当我们不希望语法被转义时就需要进行特殊处理。一般有两种解决方法:

方案一: 使用<![CDATA[ ]]>标签来包含特殊字符:

<select id="userInfo" parameterType="com.test.pojo.User" resultMap="user">   
     SELECT id,user_name,password,age,status
     FROM t_user 
     <WHERE>
     AND user_name like concat('%',#{userName},'%')
     AND age <![CDATA[ <= ]]> #{age}  
  </WHERE>
 </select>

注意: 在CDATA内部的所有内容都会被解析器忽略,保持原貌。所以在Mybatis配置文件中,要尽量缩小<![CDATA[ ]]>的作用范围,避免sql标签无法解析的问题。

方案二: 使用XML转义序列来表示特殊字符

5种特殊字符的转义序列:

image-20250106224050684

因此,上述sql也可以写成如下:

<select id="userInfo" parameterType="com.test.pojo.User" resultMap="user">   
     SELECT id,user_name, password,age,status
     FROM t_user 
     <WHERE>
     AND user_name like concat('%',#{userName},'%')
     AND age &lt;= #{age}  
  </WHERE>
 </select>

推荐使用<![CDATA[ ]]>,清晰又简洁

#{}和${}的区别

#{ }: 是预编译处理,MyBatis在处理#{ }时,它会将sql中的#{ }替换为?,然后调用PreparedStatement的set方法来赋值,传入字符串后,会在值两边加上单引号,使用占位符的方式提高效率,并且可以防止sql注入。

传入数值类型,不会加引号,这个我测试过。所以,如果你mapper层的方法和xml如下:

List<User> selectAllUsers(int i);
SELECT u.id AS user_id, u.name AS user_name, u.age AS user_age FROM #{i}user u

你传入1的话,就会执行SELECT u.id AS user_id, u.name AS user_name, u.age AS user_age FROM 1user u。

如果数据库中真的有1user,并且字段名也是匹配的,那么可以执行成功。

所以也会有sql注入的风险吗?其实是无法遇到sql注入的,因为,数据库命名一般不会是数字开头,并且你#{i}user正常写,也不会是贴在一起的,并且,你只传数字,你也无法做到输入OR '1'='1'这样的内容,就无法做到sql注入了。

${}: 表示拼接sql串,将接收到参数的内容不加任何修饰直接拼接在sql中,可能引发sql注入问题。

注意#{}的参数

当传入 java 对象类型的参数时,会根据#{参数名称}中的名称查找对象中的字段,然后将它们的值传入预处理语句的参数中。在使用#{id}完成预处理语句的设置时,还可以设置参数:

<select id="selectUser" resultType="com.***.User" parameterType="com.***.User">
  select *
  from users
  where id = #{id,javaType=double,jdbcType=NUMERIC,typeHandler=MyTypeHandler,numericScale=2}
</select>

{id,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler,numericScale=2}使用的一些属性 :

  1. id:表示具体的属性值
  2. javaType :表示参数在 java 中的数据类型
  3. jdbcType:表示参数在数据库中的数据类型
  4. typeHandler:表示使用规定的类型处理器,一般可以省略,除非该属性需要使用自定义类型处理器
  5. numericScale:数值类型,用于指定小数点后保留的位数。

还有一些其他的属性,不重要,就不介绍了。

#{}和${}的使用

#{} 作为占位符,${} 作为替换符,两者没有孰轻孰重,只不过应用场景不同,适当取舍即可。

img

#{} 能防止sql注入,${}可以替代表名和sql关键字。

例子:

例如使用 ${} 操作删除 ( 就很有问题!)

// 1、使用 ${} 有注入风险
delete from t_user where id = ${id}

// 2、正常传值,id 传入 `1`  
delete from t_user where id = 1
// 结果删除了id=1 的记录

// 3、注入风险,id 传入 `1 or 1=1` 
delete from t_user where id = 1 or 1=1
// 全表删除了

再看看 #{} 是如何规避 SQL 注入 的:

// 1、使用 #{} 有效防止注入风险
delete from t_user where id = #{id}

// 2、正常传值,id 传入 `1`   
delete from t_user where id = '1'
// 结果删除了id=1 的记录

// 3、注入风险,id 传入 `1 or 1=1` 
delete from t_user where id = '1 or 1=1'
// SQL 语句报错,表数据安全

虽然在防止 SQL 注入方面, 确 实 无 能 为 力 , 不 过 {} 确实无能为力,不过 {} 在其它方面可不容小觑,例如它允许你灵活地进行动态表和动态列名的替换操作。

例子:

# 1、灵活查询指定表数据
select * from ${tableName} 

# 传入 tableName参数 = t_user , 结果
select * from t_user  

# 2、灵活查询不同列条件数据
select * from t_user where ${colunmName} = ${value}

# 传入 colunmName参数 = name , value参数 = '潘潘', 结果
select * from t_user where name = '潘潘'

# 传入 colunmName参数 = id , value参数 = 1, 结果
select * from t_user where id = 1

以上的 ${} 替换列名与表名的方式非常灵活,不过确实存在 SQL 注入风险,所以在考虑使用 #{} 或 {} 前,需要评估风险,避免风险。

补充

类型别名

可以通过<typeAliases>标签或者@Alias("……")注解来自定义别名。一个类型可以对应多个别名,但是一个别名只能对应一个类型。

使用<typeAliases>标签定义别名的例子:

<?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>
    <!-- 日志实现 -->
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
        <!-- 驼峰命名 -->
        <setting name="mapUnderscoreToCamelCase" value="true" />
        <!-- 自动映射行为 -->
        <setting name="autoMappingBehavior" value="PARTIAL" />
<!--        autoMappingBehavior 有以下三种可选值:-->
<!--        NONE:禁用自动映射。MyBatis 不会自动将查询结果映射到 Java 对象的属性上,必须显式配置 <result> 或 <id> 等映射规则。-->
<!--        PARTIAL(默认值):部分自动映射。MyBatis 会自动映射没有嵌套结果映射的简单属性,但不会自动映射嵌套的结果(如 <association> 或 <collection>)。-->
<!--        FULL:完全自动映射。MyBatis 会尝试自动映射所有属性,包括嵌套的结果。-->
    </settings>
    <!-- 自定义类型别名 -->
    <typeAliases>
        <typeAlias alias="i1" type="java.lang.Integer"/>
        <typeAlias alias="i2" type="java.lang.Integer"/>
    </typeAliases>
</configuration>

当然,mybatis有内置了一些别名。

内置别名如下:

别名 映射类型
string java.lang.String
byte java.lang.Byte
long java.lang.Long
short java.lang.Short
int java.lang.Integer
integer java.lang.Integer
double java.lang.Double
float java.lang.Float
boolean java.lang.Boolean
byte[] java.lang.Byte[]
long[] java.lang.Long[]
short[] java.lang.Short[]
int[] java.lang.Integer[]
integer[] java.lang.Integer[]
double[] java.lang.Double[]
float[] java.lang.Float[]
boolean[] java.lang.Boolean[]
_byte byte
_long long
_short short
_int int
_integer int
_double double
_float float
_boolean boolean
_byte[] byte[]
_long[] long[]
_short[] short[]
_int[] int[]
_integer[] int[]
_double[] double[]
_float[] float[]
_boolean[] boolean[]
date java.util.Date
decimal java.math.BigDecimal
bigdecimal java.math.BigDecimal
biginteger java.math.BigInteger
object java.lang.Object
date[] java.util.Date[]
decimal[] java.math.BigDecimal[]
bigdecimal[] java.math.BigDecimal[]
biginteger[] java.math.BigInteger[]
object[] java.lang.Object[]
map java.util.Map
hashmap java.util.HashMap
list java.util.List
arraylist java.util.ArrayList
collection java.util.Collection
iterator java.util.Iterator
ResultSet java.sql.ResultSet

JDBC类型和Java类型对应表

JDBC类型和Java类型之间的关系:

JDBC Type Java Type
CHAR String
VARCHAR String
LONGVARCHAR String
NUMERIC java.math.BigDecimal
DECIMAL java.math.BigDecimal
BIT boolean
BOOLEAN boolean
TINYINT byte
SMALLINT short
INTEGER int
BIGINT long
REAL float
FLOAT double
DOUBLE double
BINARY byte[]
VARBINARY byte[]
LONGVARBINARY byte[]
DATE java.sql.Date
TIME java.sql.Time
TIMESTAMP java.sql.Timestamp
CLOB Clob
BLOB Blob
ARRAY Array
DISTINCT mapping of underlying type
STRUCT Struct
REF Ref