MyBatis 入门(一):基本用法


MyBatis(前身是 iBatis)是一个支持普通 SQL 查询、存储过程以及高级映射的持久层框架,它消除了几乎所有的JDBC 代码和参数的手动设置以及对结果集的检索,并使用简单的 XML 或注解进行配置和原始映射,用以将接口和 pojo 映射成数据库中的记录,使得 Java 开发人员可以使用面向对象的编程思想来操作数据库。MyBatis3 的官方文档地址为:https://mybatis.org/mybatis-3/zh/index.html。

快速入门

入门案例

首先查看入门案例的整体目录结构:

?? 数据库脚本

DROP TABLE IF EXISTS `t_customer`;
CREATE TABLE `t_customer` (
	`id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '客户ID',
	`username` VARCHAR(50) COMMENT '客户名',
	`job` VARCHAR(50) COMMENT '职业',
	`phone` VARCHAR(16) COMMENT '手机号码'
) COMMENT '客户表';

INSERT INTO `t_customer` VALUES(1, 'joy', 'doctor', '11111111111');
INSERT INTO `t_customer` VALUES(2, 'jack', 'teacher', '22222222222');
INSERT INTO `t_customer` VALUES(3, 'tom', 'worker', '33333333333');

?? 在 pom.xml 文件中添加相关依赖


    
    
        org.mybatis
        mybatis
        3.5.5
    

    
    
        mysql
        mysql-connector-java
        8.0.21
    

    
    
        org.projectlombok
        lombok
        1.18.12
        provided
    
    
    
    
        log4j
        log4j
        1.2.17
     

    
    
        junit
        junit
        4.12
        test
    



    
        
            org.apache.maven.plugins
            maven-compiler-plugin
            3.8.1
            
                1.8
                1.8
                UTF-8
            
        
    

?? 创建实体类

package com.example.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Customer implements Serializable {
    private Integer id;        // 客户 ID
    private String username;   // 客户名
    private String job;        // 职业
    private String phone;      // 手机号码
}

实体类属性的数据类型最好不要使用基本类型,而是使用其包装类。

?? 编写 CustomerMapper 接口,并添加 Mapper 方法

package com.example.mapper;

import com.example.entity.Customer;
import java.util.List;

public interface CustomerMapper {
    // 查询所有的客户
    List selectAll();
}

?? 编写接口对应的 XML 映射文件 CustomerMapper.xml






    
    

该映射文件位于resources/mapper目录下,文件命名任意,只要和 MyBatis 的核心配置文件中的命名统一即可,推荐和接口名一致。

?? 编写 MyBatis 的核心配置文件 mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>




    
    
        
        
            
            
            
            
                
                
                
                
                
            
        
    

    
    
        
        
    

MyBatis 核心配置文件的命名任意。

?? 编写 log4j.properteis 进行日志配置

# 配置日志的目的是在控制台输出 SQL 语句
# 将总体日志级别设置为 warn,com.example.mapper包的日志级别设置为 trace
log4j.rootCategory=warn,stdout
log4j.logger.com.example.mapper=trace
log4j.appender.stdout=org.apache.log4j.ConsoleAppender 
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout

?? 对 selectAll() 方法进行测试

package com.example.mapper;

import com.example.entity.Customer;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class CustomerMapperTest {

    private SqlSession sqlSession;
    private CustomerMapper customerMapper;

    @Before
    public void init() throws IOException {
        // 读取 MyBatis 的核心配置文件
        InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
        // 创建 SqlSessionFactory 工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        // 关闭输入流
        resourceAsStream.close();

        // 使用 SqlSessionFactory 生产 SqlSession 对象
        sqlSession = sqlSessionFactory.openSession();
        // 使用 SqlSession 创建 Mapper 接口的代理对象
        customerMapper = sqlSession.getMapper(CustomerMapper.class);
    }

    @Test
    public void selectAll() {
        // 使用代理对象执行 selectAll 方法
        List customerList = customerMapper.selectAll();
        customerList.forEach(System.out::println);
    }

    @After
    public void end() throws IOException {
        // 释放 SqlSession
        sqlSession.close();
    }
}
==>  Preparing: select id, username, job, phone from t_customer
==> Parameters: 
<==    Columns: id, username, job, phone
<==        Row: 1, joy, doctor, 11111111111
<==        Row: 2, jack, teacher, 22222222222
<==        Row: 3, tom, worker, 33333333333
<==      Total: 3
Customer(id=1, username=joy, job=doctor, phone=11111111111)
Customer(id=2, username=jack, job=teacher, phone=22222222222)
Customer(id=3, username=tom, job=worker, phone=33333333333)

执行流程分析

MyBatis 框架在操作数据库时,大体会经过了以下 8 个步骤:

  1. 读取配置文件 mybatis-config.xml,该文件是 MyBatis 的核心配置文件,配置了 MyBatis 的运行环境等信息,其中主要内容是获取数据库连接。
  2. 加载映射文件 Mapper.xml,该文件配置了操作数据库的 SQL 语句,需要在 mybatis-config.xml 中配置加载才能执行。一般而言每个 Mapper.xml 文件对应数据库中的一张表。
  3. 构建会话工厂。通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
  4. 创建 SqlSession 对象。由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 的所有方法。
  5. MyBatis 底层定义了一个 Executor 接口来操作数据库,它会根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
  6. 在 Executor 接口的执行方法中,包含一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等。Mapper.xml 文件中一个 SQL 对应一个 MappedStatement 对象,SQL 的 id 即是 MappedStatement 的 id。
  7. 输入参数映射。在执行方法时,MappedStatement 对象会对用户执行 SQL 语句的输入参数进行定义(可以定义为 Map、List 类型、基本类型和 pojo 类型),Executor 执行器会通过 MappedStatement 对象在执行 SQL 前,将输入的 Java 对象映射到 SQL 语句中。这里对输入参数的映射过程就类似于 JDBC 编程中对 preparedStatement 对象设置参数的过程。
  8. 输出结果映射。在数据库中执行完 SQL 语句后,MappedStatement 对象会对 SQL 执行输出的结果进行定义(可以定义为 Map 和 List 类型、基本类型、pojo 类型),Executor 执行器会通过 MappedStatement 对象在执行 SQL 语句后,将输出结果映射至 Java 对象中。这种将输出结果映射到 Java 对象的过程就类似于 JDBC 编程中对结果的解析处理过程。

核心配置文件配置

MyBatis 核心配置文件 mybatis-config.xml 的主要元素如下所示:

此处需要注意, 的子元素必须按上图中的由上到下的顺序配置,不可以颠倒。

?? 标签

该标签用于配置属性,通常用于将内部的配置外在化,即通过外部的配置来动态地替换内部定义的配置。例如,数据的连接信息配置。

在 resource 目录下定义 jdbc.properties 文件:

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/learning?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong
jdbc.username=root
jdbc.password=123456

根标签下配置:



此时标签就可以引用上述定义的配置:



    
    
    
    
    

?? 标签

MyBatis 在运行时有一些默认的配置,可以通过该标签更改这些默认配置,例如开启二级缓存,开启延迟加载,具体使用会出现在后续的介绍中。此处先只介绍 mapUnderscoreToCamelCase,该配置是用来开启自动驼峰命名规则(camel case)映射,默认值为 false。



    
    

开启该配置后,查询结果列会按照驼峰命名规则映射到 pojo 类的属性,例如名称为 a_column 的查询结果列会自动映射到属性 aColumn。

?? 标签

该标签是用于为配置文件中的 Java 类型设置一个简短的名字,即设置别名。具体使用如下:


    
    

设置别名之后,在 MyBatis 文件的任意位置都可以使用 Customer 来代替com.example.entity.Customer。因此,入门案例中的 selectAll() 方法对应 SQL 的返回值类型可以设置为 Customer:


上述的方式只是单个别名定义,MyBatis 还可以通过包扫描的方式来批量别名定义:



    
    

?? 标签

该标签用于指定 MyBatis 映射文件的位置,一般可以使用以下三种方式:

1)使用 classpath 路径引入


     
    

2)使用 mapper 接口类路径引入


    
    

3)使用包名引入



    
    

?? 最终的配置

数据库连接信息配置 jdbc.properties:

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/learning?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong
jdbc.username=root
jdbc.password=123456

MyBatis 的核心配置文件 mybatis-config.xml:

<?xml version="1.0" encoding="UTF-8" ?>




    
    

    
    
        
        
    

    
    
        
        
    

    
    
        
        
            
            
            
            
                
                
                
                
                
            
        
    

    
    
        
        
    


CRUD 操作

Select 用法

?? 在 CustomerMapper 接口中定义接口方法:

Customer selectById(Integer id);

?? 在 CustomerMapper.xml 映射文件中添加相应的 SQL 语句:



   id, username, job, phone



select id, username name, job, phone from t_customer where username = #{username}

标签的属性如下:

  • id:必填,唯一标识。相同,但是拥有三个特有的属性:

    • useGeneratedKeys:默认值为 false。此属性会使 MyBatis 使用 JDBC 的 getGeneratedKeys() 方法来获取由数据库内部生产的主键值,如 MySQL 自增的主键值。
    • keyProperty:此属性的作用是将插入或更新操作时的返回值赋值给 pojo 的某个属性,通常会设置为主键对应的属性。如果需要设置联合主键,可以在多个值之间用逗号隔开。
    • keyColumn:此属性用于设置第几列是主键,当主键列不是表中的第一列时需要设置。在需要主键联合时,值可以用逗号隔开。

    上述语句中,设置useGeneratedKeys="true"使得 MyBatis 使用数据库自增的主键;设置keyPropert="id"使得插入操作时自增的主键值回写到传入参数 customer 对象的 id 属性中。

    ? 对 insert2() 方法进行测试:

    @Test
    public void insert2() {
        Customer customer = new Customer(null, "Alice", "student", "55555555555");
        // 返回的是执行的 SQL 影响的行数
        int result = customerMapper.insert2(customer);
        // 影响的行数为 1
        Assert.assertEquals(1, result);
        // 自增主键回写进 customer 的 id 属性
        Assert.assertEquals(5, (long)customer.getId());
        System.out.println(customer);
    }
    
    ==>  Preparing: insert into t_customer(username, job, phone) values (?, ?, ?)
    ==> Parameters: Alice(String), student(String), 55555555555(String)
    <==    Updates: 1
    Customer(id=5, username=Alice, job=student, phone=55555555555)
    

    Update 用法

    ?? 在 CustomerMapper 接口中定义接口方法:

    int updateById(Customer customer);
    

    ?? 在 CustomerMapper.xml 中添加相应的 SQL 语句:

    
        update t_customer
        set username=#{username}, job=#{job}, phone=#{phone}
        where id=#{id}
    
    

    ?? 对 updateById() 进行测试:

    @Test
    public void updateById() {
        Customer customer = new Customer(5, "Alice", "teacher", "55555555555");
        // 返回的是执行的 SQL 影响的行数
        int result = customerMapper.updateById(customer);
        // 影响的行数为 1
        Assert.assertEquals(1, result);
    }
    
    =>  Preparing: update t_customer set username=?, job=?, phone=? where id=?
    ==> Parameters: Alice(String), teacher(String), 55555555555(String), 5(Integer)
    <==    Updates: 1
    

    Delete 用法

    ?? 在 CustomerMapper 接口中定义接口方法:

    int deleteById(Integer id);
    

    ?? 在 CustomerMapper.xml 中添加相应的 sql 语句:

    
        delete from t_customer where id=#{id}
    
    

    ?? 对 deleteById() 方法进行测试:

    @Test
    public void deleteById() {
        // 返回的是执行的 SQL 影响的行数
        int result = customerMapper.deleteById(4);
        // 影响的行数为 1
        Assert.assertEquals(1, result);
    }
    
    ==>  Preparing: delete from t_customer where id=?
    ==> Parameters: 4(Integer)
    <==    Updates: 1
    

    SQL 语句的传参

    前面已经介绍了 MyBatis 的 CURD 的简易实现,现在具体介绍下 SQL 语句的传参。SQL 语句传入参数可以是基本类型,引用类型(例如:String 类型),还可以是实体类类型(pojo 类),并且传入参数的个数可以不止一个,下面将进行讨论。

    参数绑定规则

    ?? 当传入单个参数,且参数类型为简单数据类型(基本类型和字符串类型)时,在映射文件中,#{}里可以写任意值,MyBatis 不关心这个参数叫什么,会直接把这个唯一的参数拿来使用。

    Customer selectById(Integer id);
    
    
    
    
    @Test
    public void selectById() {
        System.out.println(customerMapper.selectById(1));
    }
    
    ==>  Preparing: select id, username, job, phone from t_customer where id = ?
    ==> Parameters: 1(Integer)
    <==    Columns: id, username, job, phone
    <==        Row: 1, joy, doctor, 11111111111
    <==      Total: 1
    Customer(id=1, username=joy, job=doctor, phone=11111111111)
    

    ?? 当传入单个参数,且参数类型为 pojo 类时,直接使用#{属性名}取出 pojo 对象的属性值,使用#{pojo对象名.属性名}反而会抛出异常。需要注意,#{属性名}取值是使用 OGNL 表达式,所以相关属性要有 get 方法。

    List selectByUsernameLikeAndJob(Customer customer);
    
    
    
    @Test
    public void selectByUsernameLikeAndJob() {
        Customer customer = new Customer();
        customer.setUsername("j");
        customer.setJob("doctor");
        
        List customers = customerMapper.selectByUsernameLikeAndJob(customer);
        customers.forEach(System.out::println);
    }
    
    ==>  Preparing: select id, username, job, phone from t_customer where username like concat('%', ?, '%') and job = ?
    ==> Parameters: j(String), doctor(String)
    <==    Columns: id, username, job, phone
    <==        Row: 1, joy, doctor, 11111111111
    <==      Total: 1
    Customer(id=1, username=joy, job=doctor, phone=11111111111)
    

    ?? 当传入单个参数,且参数类型为集合或者数组时,传入的参数会被封装进一个 Map 中,如果参数类型为集合,则 key 为 collection;如果参数类型为 List,还会添加一个 list 的 key;如果参数类型为数组,则 key 为 array。在映射文件中,使用#{list}#{array}从 Map 中获取 List 集合和数组,使用#{list[0]}#{array[0]}这种索引的方式分别获取 List 和数组的第一个元素。

    List selectByUsernameLikeAndJob(List customerList);
    
    
    
    @Test
    public void selectByUsernameLikeAndJob() {
        Customer customer = new Customer();
        customer.setUsername("j");
        customer.setJob("doctor");
    
        List customers = customerMapper.selectByUsernameLikeAndJob(Arrays.asList(customer));
        customers.forEach(System.out::println);
    }
    
    ==>  Preparing: select id, username, job, phone from t_customer where username like concat('%', ?, '%') and job = ?
    ==> Parameters: j(String), doctor(String)
    <==    Columns: id, username, job, phone
    <==        Row: 1, joy, doctor, 11111111111
    <==      Total: 1
    Customer(id=1, username=joy, job=doctor, phone=11111111111)
    

    ?? 当传入多个参数时,所有参数会被封装进一个 Map,param1...paramN作为 key,对应索引位置的参数值作为 value。在映射文件中,使用#{param1}从 Map 中获取指定 key 的 value。

    List selectByUsernameLikeAndJob(String username, String job);
    
    
    
    @Test
    public void selectByUsernameLikeAndJob() {
        List customers = customerMapper.selectByUsernameLikeAndJob("j", "doctor");
        customers.forEach(System.out::println);
    }
    
    ==>  Preparing: select id, username, job, phone from t_customer where username like concat('%', ?, '%') and job = ?
    ==> Parameters: j(String), doctor(String)
    <==    Columns: id, username, job, phone
    <==        Row: 1, joy, doctor, 11111111111
    <==      Total: 1
    Customer(id=1, username=joy, job=doctor, phone=11111111111)
    

    ?? 当传入的参数被 @Param 注解指定名字时,无论参数有多少个,都会被封装进一个 Map。此时,@Param 注解指定的名字作为 key,对应参数值作为 value。

    List selectByUsernameLikeAndJob(@Param("c") Customer customer);
    
    
    

    此处需要注意,如果单纯传入单个 pojo 对象,可以直接使用#{属性名}取出对象的属性值;但是如果该 pojo 对象被 @Param 注解指定名字,那么只能使用#{指定名字.属性名}的方式取出对象的属性值。这是因为 pojo 对象被 @Param 修饰后,被封装进一个 Map,所以要先从 Map 中取出该 pojo 对象。

    @Test
    public void selectByUsernameLikeAndJob() {
        Customer customer = new Customer();
        customer.setUsername("j");
        customer.setJob("doctor");
    
        List customers = customerMapper.selectByUsernameLikeAndJob(customer);
        customers.forEach(System.out::println);
    }
    
    ==>  Preparing: select id, username, job, phone from t_customer where username like concat('%', ?, '%') and job = ?
    ==> Parameters: j(String), doctor(String)
    <==    Columns: id, username, job, phone
    <==        Row: 1, joy, doctor, 11111111111
    <==      Total: 1
    Customer(id=1, username=joy, job=doctor, phone=11111111111)
    

    多个参数的用法

    当需要传入多个参数时,可以将多个参数封装为一个 pojo 对象,但并不适合全部的情况,因为不能只为了两三个参数去创建新的 pojo 类,因此对于参数比较少的情况,还有两种方式可以采用:使用 Map 类型作为参数和使用 @Param 注解。

    ? 使用 Map 类型作为参数

    List selectByJobAndPhone(Map map);
    
    
    

    这里#{job}#{phone}参数名是传入 Map 的 key。下面对 selectByJobAndPhone() 进行测试:

    @Test
    public void selectByJobAndPhone() {
        Map map = new HashMap<>();
        map.put("job", "teacher");
        map.put("phone", "55555555555");
    
        List customers = customerMapper.selectByJobAndPhone(map);
        customers.forEach(System.out::println);
    }
    
    ==>  Preparing: select id, username, job, phone from t_customer where job=? and phone=?
    ==> Parameters: teacher(String), 55555555555(String)
    <==    Columns: id, username, job, phone
    <==        Row: 5, Alice, teacher, 55555555555
    <==      Total: 1
    Customer(id=5, username=Alice, job=teacher, phone=55555555555)
    

    ? 使用 @Param 注解

    List selectByJobAndPhone2(@Param("job") String job, @Param("phone") String phone);
    
    
    

    这里#{job}#{phone}参数名是 @Param 注解指定的名字。下面对 selectByJobAndPhone2() 进行测试:

    @Test
    public void selectByJobAndPhone2() {
        List customers = customerMapper.selectByJobAndPhone2("teacher", "55555555555");
        customers.forEach(System.out::println);
    }
    
    ==>  Preparing: select id, username, job, phone from t_customer where job=? and phone=?
    ==> Parameters: teacher(String), 55555555555(String)
    <==    Columns: id, username, job, phone
    <==        Row: 5, Alice, teacher, 55555555555
    <==      Total: 1
    Customer(id=5, username=Alice, job=teacher, phone=55555555555)
    

    需要注意:如果在动态 SQL 中使用了简单数据类型的参数作为判断条件,该参数需要使用 @Param 注解指定名字,即便只有一个参数。


    动态 SQL

    前面我们使用的 SQL 语句都是比较简单的,有些时候业务逻辑复杂时,我们的 SQL 是动态变化的,此处就需要使用动态 SQL。

    if 用法

    元素是最常用的判断语句,可类比于 Java 的 if 语句,用于实现某些简单的条件选择。

    ? where 条件中使用

    下面定义一个高级查询功能。该查询能支持以下三种情况:当只输入客户名时,根据客户名进行模糊查询;当只输入职业时,根据职业进行完全匹配;当同时输入客户名和职业时,用这两个条件去查询匹配的客户。

    List selectByCustomer(Customer customer);
    
    
    

    test 是标签中的必填属性,属性值是一个符合 OGNL 要求的判断表达式,表达式的结果为 true 或 false。为了避免 SQL 语法错误导致的异常,我们在 where 关键字后加了1 = 1条件,这种写法不美观,可以使用标签代替,后续会介绍。下面对 selectByCustomer() 方法进行测试:

    @Test
    public void selectByCustomer() {
        Customer customer = new Customer();
        customer.setUsername("j");
        customer.setJob("teacher");
    
        List customers = customerMapper.selectByCustomer(customer);
        customers.forEach(System.out::println);
    }
    
    ==>  Preparing: select id, username, job, phone from t_customer where 1 = 1 and username like concat('%', ?, '%') and job = ?
    ==> Parameters: j(String), teacher(String)
    <==    Columns: id, username, job, phone
    <==        Row: 2, jack, teacher, 22222222222
    <==      Total: 1
    Customer(id=2, username=jack, job=teacher, phone=22222222222)
    

    ? 在 update 更新列中使用

    下面定义一个选择性更新方法,只更新有变化的字段,不能将原来有值但没有发生变化的字段更新为空或 null。

    int updateByIdSelective(Customer customer);
    
    
        update t_customer
        set
        
        
            username = #{username},
        
        
            job = #{job},
        
        
            phone = #{phone},
        
        id = #{id}
        where id = #{id}
    
    

    动态 SQL 也提供了一个标签,后续会介绍。下面对 updateByIdSelective() 方法进行测试:

    @Test
    public void updateByIdSelective() {
        Customer customer = new Customer();
        customer.setId(2);
        customer.setJob("student");
        
        int result = customerMapper.updateByIdSelective(customer);
        Assert.assertEquals(1, result);
    }
    
    ==>  Preparing: update t_customer set job = ?, id = ? where id = ?
    ==> Parameters: student(String), 2(Integer), 2(Integer)
    <==    Updates: 1
    

    ? 在 insert 动态插入列中使用

    下面定义一个动态插入列的方法。在数据库表中插入数据的时候,如果某一列的参数值不为空,就使用传入的值;如果传入参数为空,就不使用传入的空值。

    int insertSelective(Customer customer);
    
    
    
        insert into t_customer(
            
            
                job,
            
            
                phone,
            
        	username)
        values(
            
                job = #{job},
            
            
                phone = #{phone},
            
        	#{username})
    
    

    对 insertSelective() 方法进行测试:

    @Test
    public void insertSelective() {
        Customer customer = new Customer();
        customer.setUsername("Bob");
    
        int result = customerMapper.insertSelective(customer);
        Assert.assertEquals(1, result);
    }
    
    ==>  Preparing: insert into t_customer( username) values( ?)
    ==> Parameters: Bob(String)
    <==    Updates: 1
    

    choose 用法

    标签能提供了基本的条件判断,但有时需要从多个选项中选择一个去执行。

    下面定义一个查询需求,当参数 id 有值优先使用 id 查询;当 id 没有值时,就去判断客户名是否有值,如果有值就用客户名查询,如果无值,那么 SQL 查询无结果。

    Customer selectByIdOrUsername(Customer customer);
    
    
    

    choose...when...otherwise标签相当于 Java 的switch...case...default,从多个选项选择一个去执行。一个 choose 中至少有一个 when,有 0 个或者 1 个 otherwise。下面对 selectByIdOrUsername() 方法进行测试:

    @Test
    public void selectByIdOrUsername() {
        Customer customer = new Customer();
        customer.setUsername("jack");
        
        System.out.println(customerMapper.selectByIdOrUsername(customer));
    }
    
    ==>  Preparing: select id, username, job, phone from t_customer where 1 = 1 and username = ?
    ==> Parameters: jack(String)
    <==    Columns: id, username, job, phone
    <==        Row: 2, jack, student, 22222222222
    <==      Total: 1
    Customer(id=2, username=jack, job=student, phone=22222222222)
    

    where、set、trim 用法

    这 3 个标签解决了类似的问题,并且都属于的一种特殊用法。

    where 用法

    使用标签重写 selectByCustomer() 方法对应的 SQL:

    
    

    set 用法

    使用标签重写 updateByIdSelective() 方法对应的 SQL:

    
        update t_customer
        
        
            
                username = #{username},
            
            
                job = #{job},
            
            
                phone = #{phone},
            
            id = #{id},
        
        where id = #{id}
    
    

    标签的用法中,SQL 后面的逗号会自动去除,但是如果 set 元素中没有内容,仍会出现 SQL 错误,所以为了避免错误产生,类似id=#{id}这样条件还需保留。

    trim 用法

    标签的功能都可以用 trim 标签来实现。

    ?? 标签对应实现如下:

    
        ...
    
    

    注意:这里的 and 和 or 后面的空格不能省略,为了避免匹配到 andes、orders 等单词。

    ?? 标签对应实现如下:

    
        ...
    
    

    ?? 标签属性

    • prefix:当标签内包含内容时,会给内容增加 prefix 指定的前缀。
    • prefixOverrides:当标签内包含内容时,会把内容中匹配的前缀字符串去掉。
    • suffix:当标签内包含内容时,会给内容增加 suffix 指定的后缀。
    • suffixOverrides:当标签内包含内容时,会把内容中匹配的后缀字符串去掉。

    foreach 用法

    基本使用

    元素通常在构建 in 条件语句时使用,用于数组和集合循环遍历。

    下面定义一个根据传入的客户 id 集合查询出所有符合条件的客户方法:

    List findCustomerByIds(List ids);
    
    
    

    标签属性如下:

    • collection:必填,值为要选代循环的属性名。这个属性值的情况有很多。
    • item:变量名,值为从迭代对象中取出的每一个值。
    • index:索引的属性名,在集合数组情况下值为当前索引值, 当选代循环的对象是 Map 类型时,这个值为 Map 的 key (键值)。
    • open/close:整个循环内容开头/结尾的字符串。
    • separator:每次循环的分隔符。

    在使用时,collection 属性必须设置。在不同情况下,该属性值不一样的:

    ?? 传入的单个参数类型为集合或数组

    前面也提到,当传入的单个参数类型是集合或数组时,会被封装进一个 Map:

    • 如果参数类型是集合,会添加一个 key 为 collection 的值;如果参数类型是 List 集合,还会继续添加一个 key 为 list 的值;因此,当需要遍历传入的 List 集合时,设置collection = "list"
    • 如果参数类型是数组,会添加一个 key 为 array 的值。因此,当需要遍历数组时,设置collection = "array"

    上面提到的是数组或集合参数在 Map 中的默认名字,推荐使用 @Param 来指定名字,这时 collection 就可以设置为 @Param 注解指定的名字。

    ?? 传入多个参数

    当有多个参数的时候,一般需要使用 @Param 注解给每个参数指定一个名字,否则在 SQL 中使用参数就会不方便,因此将 collection 设置为 @Param 注解指定的名字即可。

    ?? 传入的参数是 Map

    使用 Map 和使用 @Param 注解方式类似,如果想循环的是 Map 中的某个 value,将 collection 指定为对应的 key 即可。如果要循环的是所传入的 Map,推荐使用 @Param 注解指定名字,此时可将 collection 设置为指定的名字;如果不想指定名字,就使用默认值_parameter_parameter是 MyBatis 的内置参数,代表整个传入参数。

    ?? 传入参数是 pojo 对象

    这种情况下指定为对象的属性名即可。当使用对象内多层嵌套的对象时,使用属性.属性(集合和数组可以使用下标取值)的方式可以指定深层的属性值。

    对 findCustomerByIds() 方法进行测试:

    @Test
    public void findCustomerByIds() {
        List ids = new ArrayList<>();
        ids.add(1);
        ids.add(2);
    
        List customers = customerMapper.findCustomerByIds(ids);
        customers.forEach(System.out::println);
    }
    
    ==>  Preparing: select id, username, job, phone from t_customer where id in ( ? , ? )
    ==> Parameters: 1(Integer), 2(Integer)
    <==    Columns: id, username, job, phone
    <==        Row: 1, joy, doctor, 11111111111
    <==        Row: 2, jack, student, 22222222222
    <==      Total: 2
    Customer(id=1, username=joy, job=doctor, phone=11111111111)
    Customer(id=2, username=jack, job=student, phone=22222222222)
    

    实现批量插入

    int insertList(List customerList);
    
    
        insert into t_customer(username, job, phone) values
        
            (#{c.username}, #{c.job}, #{c.phone})
        
    
    

    对 insertList() 进行测试:

    @Test
    public void insertListTest() {
        List customerList = new ArrayList<>();
        customerList.add(new Customer(null, "A", "Student", null));
        customerList.add(new Customer(null, "B", "Student", null));
        customerList.add(new Customer(null, "C", "Student", null));
        int result = customerMapper.insertList(customerList);
        Assert.assertEquals(3, result);
    }
    
    ==>  Preparing: insert into t_customer(username, job, phone) values (?, ?, ?) , (?, ?, ?) , (?, ?, ?)
    ==> Parameters: A(String), Student(String), null, B(String), Student(String), null, C(String), Student(String), null
    <==    Updates: 3
    

    实现动态更新

    int updateByMap(@Param("map") Map map);
    
    
        update t_customer
        set
        
            ${key} = #{val}
        
        where id = #{map.id}
    
    

    对 updateByMap() 方法进行测试:

    @Test
    public void updateByMap() {
        Map map = new HashMap<>();
        map.put("id", 1);
        map.put("phone", "1234566777");
    
        int result = customerMapper.updateByMap(map);
        Assert.assertEquals(1, result);
    }
    
    ==>  Preparing: update t_customer set phone = ? , id = ? where id = ?
    ==> Parameters: 1234566777(String), 1(Integer), 1(Integer)
    <==    Updates: 1
    

    bind 用法

    元素可以通过 OGNL 表达式来创建一个上下文变量。

    List selectByNameLike(Customer customer);
    
    
    

    对 selectByNameLike() 方法进行测试:

    @Test
    public void testSelectByNameLike() {
        Customer c = new Customer();
        c.setUsername("j");
    
        List customers = customerMapper.selectByNameLike(c);
        customers.forEach(System.out::println);
    }
    
    ==>  Preparing: select id, username, job, phone from t_customer where username like ?
    ==> Parameters: %j%(String)
    <==    Columns: id, username, job, phone
    <==        Row: 1, joy, doctor, 1234566777
    <==        Row: 2, jack, student, 22222222222
    <==      Total: 2
    Customer(id=1, username=joy, job=doctor, phone=1234566777)
    Customer(id=2, username=jack, job=student, phone=22222222222)
    

    内置参数_paramter

    MyBatis 里存在一个内置参数_paramter,该内置参数表示整个传入参数。

    • 当传入单个参数时,_parameter就是这个参数;
    • 当传入多个参数时,参数会被封装为一个 map,_parameter就是代表这个map。

    下面对 selectByCustomer() 方法对应的 SQL 进行重写:

    List selectByCustomer(Customer customer);
    
    
    

    上述语句中,_parameter代表着传入的参数 customer,可以使用_parameter.属性名获取其属性值。


    参考

    1. 《MyBatis从入门到精通》
    2. 《Java EE企业级应用开发教程(Spring+Spring MVC+MyBatis)》