MyBatis 基本使用

5/6/2022 MyBatis

摘要

JDK:1.8.0_202
MyBatis Version:3.5.9

# 一:前言

由于Java中的基本类型会有默认值,例如当某个类中存在 private int age; 字段时,创建这个类时,age会有默认值0。在某些情况下,便无法实现使age为null。并且在动态SQL的部分,如果使用 age != null 进行判断,结果总会为true,因而会导致很多隐藏的问题。
所以,在实体类中不要使用基本类型。基本类型包括byte、int、short、long、float、double、char、boolean。

使用下面内容时,需要调节上一章节的日志控制,在 mybatis-config.xml 中,更改 <setting name="logImpl" value="STDOUT_LOGGING"/>

# 二:XML

MyBatis 3.0相比2.0版本的一个最大变化,就是支持使用接口来调用方法。

以前使用SqlSession通过命名空间调用MyBatis方法时,首先需要用到命名空间和方法id 组成的字符串来调用相应的方法。当参数多于 1 个的时候,需要将所有参数放到一个 Map对象中。通过Map传递多个参数,使用起来很不方便,而且还无法避免很多重复的代码。

使用接口调用方式就会方便很多,MyBatis使用Java的动态代理可以直接通过接口来调用相应的方法,不需要提供接口的实现类,更不需要在实现类中使用SqlSession以通过命名空间间接调用。另外,当有多个参数的时候,通过参数注解@Param设置参数的名字省去了手动构造Map参数的过程,尤其在Spring中使用的时候,可以配置为自动扫描所有的接口类,直接将接口注入需要用到的地方。

# 2.1 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="top.qform.mapper.CustomersMapper">
</mapper>
1
2
3
4

<mapper>根标签的namespace属性,当Mapper接口和XMLL文件关联的时候,命名空间namespace的值需要配置成接口的全限定名称。MyBatis 内部就是通过这个值将接口和XML关联起来的。如果没有接口,则改成XML自身全路径限定,例如 src/main/resources/mapper/CustomersMapper.xml 则改为 \<mapper namespace="mapper.CustomersMapper">

# 2.2 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="top.qform.mapper.CustomersMapper">
    <resultMap id="BaseResultMap" type="top.qform.model.Customers">
        <id column="cust_id" jdbcType="INTEGER" property="custId"/>
        <result column="cust_name" jdbcType="CHAR" property="custName"/>
        <result column="cust_address" jdbcType="CHAR" property="custAddress"/>
    </resultMap>
</mapper>
1
2
3
4
5
6
7
8
9

<resultMap> — 用于配置 Java 对象的属性和查询结果列的对应关系。下面是该标签所拥有的属性。

  • id:必填,唯一标识,在select标签中,resultMap指定的值即为此处id所设置的值;
  • type:必填,用于配置查询列所映射到的Java对象类型;
  • extends:选填,可以配置当前的resultMap继承自其他的resultMap,属性值为继承resultMap的id;
  • autoMapping:选填,可选值为true或false,用于配置是否启用非映射字段(没有在 resultMap 中配置的字段)的自动映射功能,该配置可以覆盖全局的autoMappingBehavior配置。

下面是 <resultMap> 标签的子标签:

  • constructor:配置使用构造方法注入结果,包含以下两个子标签,对应 resultMap 的 id、result 标签,含义相同,注入方式不同;
    • idArg:id参数,标记结果作为id(唯一值),可以帮助提高整体性能,对应 resultMap 的 id;
    • id:注入到构造方法的一个普通结果,对应 resultMap 的 result。
  • id:一个id结果,标记结果作为id(唯一值),可以帮助提高整体性能;
    • column:从数据库中得到的列名,或者是列的别名;
    • property:映射到列结果的属性。可以映射简单的如 "username" 这样的属性,也可以映射一些复杂对象中的属性,例如 "address.street.number",这会通过 "." 方式的属性嵌套赋值;
    • javaType:一个Java类的完全限定名,或一个类型别名(通过typeAlias配置或者默认的类型)。如果映射到一个 JavaBean,MyBatis 通常可以自动判断属性的类型。如果映射到HashMap,则需要明确地指定javaType属性;
    • jdbcType:列对应的数据库类型。JDBC 类型仅仅需要对插入、更新、删除操作可能为空的列进行处理。这是JDBC jdbcType的需要,而不是MyBatis的需要;
    • typeHandler:使用这个属性可以覆盖默认的类型处理器。这个属性值是类的完全限定名或类型别名。
  • result:注入到Java对象属性的普通结果,属性与id属性一样,属性值是通过setter方法注入。两标签不同在于id代表主键;
  • association:一个复杂的类型关联,许多结果将包成这种类型;
  • collection:复杂类型的集合;
  • discriminator:根据结果值来决定使用哪个结果映射;
  • case:基于某些值的结果映射。

# 2.3 select

customersMapper.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="top.qform.mapper.CustomersMapper">
    <resultMap id="BaseResultMap" type="top.qform.model.Customers">
        <id column="cust_id" jdbcType="INTEGER" property="custId"/>
        <result column="cust_name" jdbcType="CHAR" property="custName"/>
        <result column="cust_address" jdbcType="CHAR" property="custAddress"/>
    </resultMap>

    <sql id="Base_Column_List">
        cust_id, cust_name, cust_address
    </sql>

    <select id="selectById" resultMap="BaseResultMap">
        SELECT <include refid="Base_Column_List" /> FROM customers WHERE cust_id = #{id}
    </select>
    
    <select id="selectAll" resultType="top.qform.model.Customers">
        SELECT cust_id AS custId, cust_name AS custName, cust_address AS custAddress FROM customers
    </select>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

CustomersMapper.java

public interface CustomersMapper {

	Customers selectById(Integer id);
	
	List<Customers> selectAll();

}
1
2
3
4
5
6
7

Customers.java

public class Customers {

	private Integer custId;
	private String custName;
	private String custAddress;
	
	// getter 和 setter
}
1
2
3
4
5
6
7
8

测试类:

public class CustomersTest {

    private static SqlSessionFactory sqlSessionFactory;

    @BeforeClass
    public static void init() {
        try (Reader reader = Resources.getResourceAsReader("mybatis-config.xml")) {
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void testSelectAll() {
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            // 如果下面语句报错,需要指定全限定 List<Customers> customers = sqlSession.selectList("top.qform.mapper.CustomersMapper.selectAll");
            List<Customers> customers = sqlSession.selectList("selectAll");
            customers.forEach(System.out::println);
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

接口和XML是通过将namespace的值为接口的全限定名称来进行关联

虽然方法有重载,但是XML中的值不能重复,因此XML对应的接口里面的方法重载会报错。

<select> — 映射查询语句使用的标签,下面是该标签所拥有的属性。

  • id:命名空间中的唯一标识符,用来代表这条语句的标识;
  • resultMap:用于设置返回值的类型和映射关系。
  • resultMap:用于设置返回值的类型

名称映射规则

仔细观察上面的 selectById 和 selectAll,其中 selectById 使用了 resultMap 来设置结果映射,而 selectAll 中则通过 resultType 直接指定了返回结果类型,但需要在 DML 中为所有列名和属性不一致的列设置别名,使得最终的查询结果列和resultType指定的对象属性名保持一致,进而实现自动映射。

如果不用别名,仅仅是大小驼峰的区别,可以修改MyBatis配置,开启自动驼峰命名规则,如,在自定义的 mybatis-config.xml 配置可以如下配置:

<configuration>
	<settings>
		<setting name="mapUnderscoreToCamelCase" value="true"/>
	</settings>
</configuration>
1
2
3
4
5

在实际匹配的时候,MyBatis会先将两者都转换为大写形式,然后判断是否相同,即 property="userName" 和 property="userName" 都可以匹配到对象的userName属性上。判断是否相同的时候要使用USERNAME,因此在设置property属性或别名的时候,不需要考虑大小写是否一致,但是为了便于阅读,要尽可能按照统一的规则来设置。

含拓展字段:

在联表查询时候,可能需要其他表的一些字段结果返回。这时候 POJO 有几种处理方式。以上面 Customers 为例。

方法一:直接在 Customers 添加字段 userName 方法二:继承实现

public class CustomersExtend extends Customers {

	private String userName;
	
	// getter 和 setter
}
1
2
3
4
5
6

方法三:上面两种方法适合少量字段,拓展字段多,可以考虑组合

public class Customers {
	
	// 其他原有字段
	
	// 用户信息表(userName 来源)
	private User user;

}
1
2
3
4
5
6
7
8

xml中使用别名时,需要指定为 "字段名.属性名",例如下面 "user.userName",通过这种方式,将值赋予user字段中的userName属性。




 



<select id="selectAll" resultType="top.qform.model.Customers">
	SELECT 
		c.cust_id AS custId, c.cust_name AS custName, c.cust_address AS custAddress, 
        u.user_name AS user.userName
		FROM customers c LEFT JOIN user u ON c.cust_id = u.cust_id
</select>
1
2
3
4
5
6

# 2.4 insert

CustomersMapper.xml

<insert id="insert" parameterType="top.qform.model.Customers">
	insert into customers (cust_name, cust_address)
	values (#{custName}, #{custAddress,jdbcType=CHAR})
</insert>
1
2
3
4

CustomersMapper.java

public interface CustomersMapper {

    int insert(Customers customers);

}
1
2
3
4
5

测试类:

@Test
public void testInsert() {
	SqlSession sqlSession = null;
	try {
		sqlSession = sqlSessionFactory.openSession();
		CustomersMapper mapper = sqlSession.getMapper(CustomersMapper.class);
		Customers customers = new Customers();
		customers.setCustAddress("gd gz");
		customers.setCustName("gd");
		// 返回值 result 是执行的 SQL 影响的行数
		int result = mapper.insert(customers);
		// 只插入1条数据
		Assert.assertEquals(1, result);
	} finally {
		// 为了不影响其他测试,这里选择回滚
		// 由于默认的 sqlSessionFactory.openSession() 是不会自动提交的
		// 因此不手动执行 commit 也不会提交到数据库
		assert sqlSession != null;
		sqlSession.rollback();
		// 关闭sqlSession
		sqlSession.close();
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

为了防止类型错误,对于一些特殊的数据类型,建议指定具体的 jdbcType 值。例如 BLOB类型,TIMESTAMP类型等。BLOB对应的类型是ByteArrayInputStream,就是二进制数据流。 由于数据库区分date、time、datetime类型,但是Java中一般都使用java.util.Date类型。因此为了保证数据类型的正确,需要手动指定日期类型,date、time、datetime对应的JDBC类型分别为DATE、TIME、TIMESTAMP。

<insert> — 映射插入语句使用的标签,下面是该标签所拥有的属性。

  • id:命名空间中的唯一标识符,可用来代表这条语句。
  • parameterType:即将传入的语句参数的完全限定类名或别名。这个属性是可选的,因为MyBatis可以推断出传入语句的具体参数,因此不建议配置该属性。
  • flushCache:默认值为true,任何时候只要语句被调用,都会清空一级缓存和二级缓存。
  • timeout:设置在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。
  • statementType:对于STATEMENT、PREPARED、CALLABLE,MyBatis会分别使用对应的 Statement、PreparedStatement、CallableStatement,默认值为PREPARED。
  • useGeneratedKey:默认值为 false。如果设置为 true,MyBatis 会使用 JDBC的getGeneratedKeys方法来取出由数据库内部生成的主键。
  • keyProperty:MyBatis通过getGeneratedKeys获取主键值后将要赋值的属性名。如果希望得到多个数据库自动生成的列,属性值也可以是以逗号分隔的属性名称列表。
  • keyColumn:仅对INSERT和UPDATE有用。通过生成的键值设置表中的列名,这个设置仅在某些数据库(如 PostgreSQL)中是必须的,当主键列不是表中的第一列时需要设置。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
  • databaseId:如果配置了databaseIdProvider,MyBatis会加载所有的不带databaseId的或匹配当前databaseId的语句。如果同时存在带databaseId和不带databaseId的语句,后者会被忽略。

获取组件自增的值

 




<insert id="insert2" useGeneratedKeys="true" keyProperty="custId">
	insert into customers (cust_name, cust_address)
	values (#{custName}, #{custAddress,jdbcType=CHAR})
</insert>
1
2
3
4

useGeneratedKeys 设置为true后,MyBatis会使用JDBC的 getGeneratedKeys 方法来取出由数据库内部生成的主键。获得主键值后将其赋值给 keyProperty 配置的 custId 属性。当需要设置多个属性时,使用逗号隔开,这种情况下通常还需要设置 keyColumn 属性,按顺序指定数据库的列,这里列的值会和keyProperty配置的属性一一对应。

单元测试

// 返回值 result 是执行的 SQL 影响的行数
int result = mapper.insert2(customers);
// 只插入1条数据
Assert.assertEquals(1, result);
System.out.println(customers.getCustId());
1
2
3
4
5

# 2.5 selectKey

上面这种回写主键的方法只适用于支持主键自增的数据库。有些数据库(如 Oracle)不提供主键自增的功能,而是使用序列得到一个值,然后将这个值赋给id,再将数据插入数据库。对于这种情况,可以采用另外一种方式:使用 <selectKey> 标签来获取主键的值,这种方式不仅适用于不提供主键自增功能的数据库,也适用于提供主键自增功能的数据库。

MySQL

<insert id="insert3">
	insert into customers (cust_name, cust_address)
	values (#{custName}, #{custAddress,jdbcType=CHAR})
	<selectKey keyColumn="cust_id" resultType="int" keyProperty="custId" order="AFTER">
		SELECT LAST_INSERT_ID()
	</selectKey>
</insert>
1
2
3
4
5
6
7

Oracle

<insert id="insert3">
	<selectKey keyColumn="cust_id" resultType="int" keyProperty="custId" order="BEFORE">
		SELECT SEQ_ID.nextval from dual
	</selectKey>
	insert into customers (cust_name, cust_address)
	values (#{custName}, #{custAddress,jdbcType=CHAR})
</insert>
1
2
3
4
5
6
7

selectKey元素的位置放在insert之前或之后,不会影响selectKey中的方法在insert前面或者后面执行的顺序,影响执行顺序的是 order 属性

Oracle方式的INSERT语句中明确写出了id列和值#{id},因为执行selectKey中的语句后 custId 就有值了,需要把这个序列值作为主键值插入到数据库中,所以必须指定 custId 列,如果不指定这一列,数据库就会因为主键不能为空而抛出异常。

以下是其他一些支持主键自增的数据库配置 selectKey 中回写主键的 SQL。

  • DB2VALUES IDENTITY_VAL_LOCAL()
  • MYSQLSELECT LAST_INSERT_ID()
  • SQLSERVERSELECT SCOPE_IDENTITY()
  • CLOUDSCAPEVALUES IDENTITY_VAL_LOCAL()
  • DERBYVALUES IDENTITY_VAL_LOCAL()
  • HSQLDBCALL IDENTITY()
  • SYBASESELECT@@IDENTITY()
  • DB2_MFSELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1
  • INFORMIXselect dbinfo('sqlca.sqlerrd1') from systables where tabid=1

# 2.6 update

CustomersMapper.xml

<update id="updateById">
	UPDATE customers 
	SET cust_name = #{custName}, cust_address = #{custAddress}
	WHERE cust_id = #{custId}
</update>
1
2
3
4
5

CustomersMapper.java

public interface CustomersMapper {

	int updateById(Customers customers);

}
1
2
3
4
5

# 2.7 delete

CustomersMapper.xml

<delete id="deleteById" >
	DELETE FROM customers
	WHERE cust_id = #{custId}
</delete>
1
2
3
4

CustomersMapper.java

public interface CustomersMapper {

	int deleteById(Integer custId);

}
1
2
3
4
5

# 三:入参

当 Mapper.xml 中有多个参数时,对应的 Mapper 接口有几种入参写法。

  • 单个入参,例如上面的 deleteById 方法,可以普通入参,直接使用;
  • 多个入参
    • JavaBean,例如上面的 insert、update 方法;
    • Map,通过Map的key-value方式传递参数值,key来映射XML中SQL使用的参数值名字,value用来存放参数值,由于这种方式还需要手动创建Map以及对参数进行赋值,其实并不简洁,故而不推荐;
    • @Param

# 3.1 异常

如果多个参数,但是不用上面说的几种方式,就当作普通入参会如何?

修改上面的insert方法

CustomersMapper.xml

<insert id="insert">
	insert into customers (cust_name, cust_address)
	values (#{custName}, #{custAddress})
</insert>
1
2
3
4

CustomersMapper.java

public interface CustomersMapper {
    int insert(String custName, String custAddress);
}
1
2
3

运行结果

可以发现 PersistenceExceptionBindingException,根据 BindingException 错误提示,XML可用的参数只有 [arg1, arg0, param1, param2],这时候如果XML中的 #{custName} 换成 #{arg0}param1 和 #{custAddress} 换成 #{arg1}param2 就会发现正常调用,不过不建议这么使用。

# 3.2 @Param

import org.apache.ibatis.annotations.Param;

public interface CustomersMapper {

    int insert(@Param("custName") String custName, @Param("custAddress") String custAddress);

}
1
2
3
4
5
6
7

给参数配置@Param注解后,MyBatis就会自动将参数封装成Map类型,@Param注解值会作为Map中的key,因此在SQL部分就可以通过配置的注解值来使用参数。

当只有一个参数(基本类型或拥有TypeHandler配置的类型)的时候,MyBatis不关心这个参数叫什么名字就会直接把这个唯一的参数值拿来使用。

# 3.3 多个JavaBean

如果入参有多个JavaBean,这时候主要XML会变化的比较大,例如:

CustomersMapper.java

List<Customers> selectCondition(@Param("customer") Customers customer, @Param("user") User user)
1

CustomersMapper.xml

<select id="selectAll" resultType="top.qform.model.Customers">
	SELECT 
		c.cust_id AS custId, c.cust_name AS custName, c.cust_address AS custAddress, 
        u.user_name AS user.userName
		FROM customers c LEFT JOIN user u ON c.cust_id = u.cust_id
		WHERE user_name = #{user.userName} AND cust_name = #{customer.custName}
</select>
1
2
3
4
5
6
7

# 四:注解

MyBatis注解方式就是将SQL语句直接写在接口上。这种方式的优点是,对于需求比较简单的系统,效率较高。缺点是,当SQL有变化时都需要重新编译代码,一般情况下不建议使用注解方式

# 4.1 @Select

public interface Customers2Mapper {

    @Select("SELECT cust_id, cust_name, cust_address FROM customers WHERE cust_id = #{id}")
    Customers selectById1(Integer id);

}
1
2
3
4
5
6

映射转换:

  1. SQL使用别名
  2. 配置加入mapUnderscoreToCamelCase,自动转换大小驼峰
  3. @Results

# 4.2 @Results

@Results({
    @Result(property = "custId", column = "cust_id", id = true),
    @Result(property = "custName", column = "cust_name"),
    @Result(property = "custAddress", column = "cust_address")
})
@Select("SELECT cust_id, cust_name, cust_address FROM customers WHERE cust_id = #{id}")
Customers selectById2(Integer id);
1
2
3
4
5
6
7
  • @Results 对应XML中的 <resultMap> 元素
  • @Result 对应XML中的 <result> 元素
  • id=true 对应 <id> 元素

MyBatis 3.3.1 之后,@Results 注解增加了一个id属性,设置了id属性后,就可以通过id属性引用同一个 @Results 配置了

Results(id = "customerBase", value = {
    @Result(property = "custId", column = "cust_id", id = true),
    @Result(property = "custName", column = "cust_name"),
	@Result(property = "custAddress", column = "cust_address")
})
@Select("SELECT cust_id, cust_name, cust_address FROM customers WHERE cust_id = #{id}")
Customers selectById2(Integer id);

@ResultMap("customerBase")
@Select("SELECT cust_id, cust_name, cust_address FROM customers")
Customers selectAll();
1
2
3
4
5
6
7
8
9
10
11

测试:

public class CustomersTest3 {

    private static SqlSessionFactory sqlSessionFactory;

    @BeforeClass
    public static void init() {
        try (Reader reader = Resources.getResourceAsReader("mybatis-config.xml")) {
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void testSelectById2() {
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            Customers2Mapper mapper = sqlSession.getMapper(Customers2Mapper.class);
            // 调用 selectById2 方法,查询 id = 10001 的信息
            Customers customers = mapper.selectById2(10001);
            Assert.assertNotNull(customers);
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

如果直接运行,就会发现报如下错误,所以还需要指定mapper

mybatis-config.xml


 


<mappers>
    <mapper class="top.qform.mapper.Customers2Mapper"/>
</mappers>
1
2
3

# 4.3 @Insert

普通用法

@Insert("insert into customers (cust_name, cust_address) values (#{custName}, #{custAddress,jdbcType=CHAR})")
int insert1(Customers customers);
1
2

返回自增主键


 


@Insert("insert into customers (cust_name, cust_address) values (#{custName}, #{custAddress,jdbcType=CHAR})")
@Options(useGeneratedKeys = true, keyProperty = "custId")
int insert2(Customers customers);
1
2
3

返回非自增主键


 


@Insert("insert into customers (cust_name, cust_address) values (#{custName}, #{custAddress,jdbcType=CHAR})")
@SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "custId", resultType = Integer.class, before = false)
int insert3(Customers customers);
1
2
3

before 为 false 时功能等同于 order="AFTER",before为true时功能等同于 order="BEFORE"

测试:

@Test
public void testInsert() {
    SqlSession sqlSession = null;
    try {
        sqlSession = sqlSessionFactory.openSession();
        Customers2Mapper mapper = sqlSession.getMapper(Customers2Mapper.class);
        Customers customers = new Customers();
        customers.setCustId(33);
        customers.setCustAddress("gd gz");
        customers.setCustName("gd");
        int result = mapper.insert2(customers);
        System.out.println(customers.getCustId());
        result = mapper.insert3(customers);
        System.out.println(customers.getCustId());
    } finally {
        // 为了不影响其他测试,这里选择回滚
        // 由于默认的 sqlSessionFactory.openSession() 是不会自动提交的
        // 因此不手动执行 commit 也不会提交到数据库
        assert sqlSession != null;
        sqlSession.rollback();
        // 关闭sqlSession
        sqlSession.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 4.4 @Update

@Update("UPDATE customers SET cust_name = #{custName}, cust_address = #{custAddress} WHERE cust_id = #{custId}")
int updateById(Customers customers);
1
2

# 4.5 @Delete

@Delete("insert into customers (cust_name, cust_address) values (#{custName}, #{custAddress})")
int deleteById(Integer custId);
1
2

# 4.6 Provider

  • @SelectProvider
  • @InsertProvider
  • @UpdateProvider
  • @DeleteProvider

CustomersProvider.java

public class CustomersProvider {

    public String selectById(final Integer id) {
        return new SQL() {
            {
                SELECT("cust_id, cust_name, cust_address");
                FROM("customers");
                WHERE("cust_id = #{id}");
            }
        }.toString();
    }

    public String selectAll() {
        return "SELECT cust_id, cust_name, cust_address FROM customers";
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Customers3Mapper.java

public interface Customers3Mapper {

    @SelectProvider(type = CustomersProvider.class, method = "selectById")
    Customers selectById(Integer id);

    @SelectProvider(type = CustomersProvider.class, method = "selectAll")
    Customers selectAll();

}
1
2
3
4
5
6
7
8
9

mybatis-config.xml


 


<mappers>
	<mapper class="top.qform.mapper.Customers3Mapper"/>
</mappers>
1
2
3

测试

public class CustomersTest4 {

    private static SqlSessionFactory sqlSessionFactory;

    @BeforeClass
    public static void init() {
        try (Reader reader = Resources.getResourceAsReader("mybatis-config.xml")) {
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void testSelectAll() {
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            Customers3Mapper mapper = sqlSession.getMapper(Customers3Mapper.class);
            // 调用 selectById2 方法,查询 id = 10001 的信息
            Customers customers = mapper.selectById(10001);
            Assert.assertNotNull(customers);
            // 返回全部
            System.out.println(mapper.selectAll().size());
        }
    }
    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 五:动态SQL

MyBatis3 之后的版本采用了功能强大的 OGNL(Object-Graph Navigation Language)表达式语言,以下是MyBatis的动态SQL在XML中支持的几种标签。

  • if
  • choose(when、oterwise)
  • trim(where、set)
  • foreach
  • bind

# 5.1 if

通常用于 WHERE 语句中,通过判断参数值来决定是否使用某个查询条件,也经常用于 UPDATE 语句中判断是否更新某一个字段,和在 INSERT 语句中用来判断是否插入某个字段的值。

CustomersMapper.xml

<select id="selectCondition" resultMap="BaseResultMap">
    SELECT <include refid="Base_Column_List" />
    FROM customers
    WHERE 1 = 1
    <if test="null != custName and '' != custName">
    	and cust_name LIKE CONCAT('%', #{custName} ,'%')
    </if>
    <if test="null != custAddress and '' != custAddress">
    	and cust_address LIKE CONCAT('%', #{custAddress} ,'%')
    </if>
</select>
1
2
3
4
5
6
7
8
9
10
11

CustomersMapper.java

public interface CustomersMapper {
	List<Customers> selectCondition(Customers customers);
}
1
2
3

测试:

@Test
public void testSelectCondition() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
        CustomersMapper mapper = sqlSession.getMapper(CustomersMapper.class);
        Customers customers = new Customers();
        customers.setCustAddress("gd");
        customers.setCustName("gd");
        System.out.println(mapper.selectCondition(customers));
    }
}
1
2
3
4
5
6
7
8
9
10
  • test属性必填,表达式结果可以是true或false,除此之外所有的非0值都为true,只有0为false。
  • 判断条件 property != nullproperty == null,适用于任何类型的字段,用于判断属性值是否为空。
  • 判断条件 property != ''property == '',仅适用于String类型的字段,用于判断是否为空字符串。
  • and和or:当有多个判断条件时,使用and或or进行连接,嵌套的判断可以使用小括号分组,and相当于Java中的与(&&),or相当于Java中的或(||)

# 5.2 choose

choose 元素中包含 whenotherwise 两个标签,至少有一个 when。效果 if...else...

<select id="selectCondition" resultMap="BaseResultMap">
    SELECT <include refid="Base_Column_List" />
    FROM customers
    WHERE 1 = 1
    <choose>
        <when test="null != custId">
        	and cust_id = #{custId}
        </when>
        <when test="null != custName and '' != custName">
        	and cust_name LIKE CONCAT('%', #{custName} ,'%')
        </when>
        <otherwise>
        	and 1 = 2
        </otherwise>
    </choose>
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上面的SQL,当参数 custId 有值的时候优先使用 custId 查询;当 custId 无值时就去判断 custName 是否有值,如果有值就用 custName;如果再无值, 则走 otherwise 恒为false,查询不到结果。

# 5.3 where

where和set都属于trim的一种具体用法

where 标签作用,如果该标签包含的元素中有返回值,就插入一个where;如果where后面的字符串是以 AND 和 OR 开头的,就将它们剔除。






 


 




<select id="selectCondition" resultMap="BaseResultMap">
    SELECT <include refid="Base_Column_List"/>
    FROM customers
    <where>
        <if test="null != custName and '' != custName">
        	and cust_name LIKE CONCAT('%', #{custName} ,'%')
        </if>
        <if test="null != custAddress and '' != custAddress">
        	and cust_address LIKE CONCAT('%', #{custAddress} ,'%')
        </if>
    </where>
</select>
1
2
3
4
5
6
7
8
9
10
11
12

# 5.4 set

set标签作用,如果该标签包含的元素中有返回值,就插入一个set;如果set后面的字符串是以逗号结尾的,就将这个逗号剔除。

<update id="updateById">
    UPDATE customers
    <set>
        <if test="null != custId and '' != custId">
        	cust_id = #{custId},
        </if>
        <if test="null != custName and '' != custName">
        	cust_name = #{custName},
        </if>
        <if test="null != custAddress and '' != custAddress">
        	cust_address = #{custAddress}
        </if>
    </set>
    WHERE custId = #{custId}
</update>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

全部的查询条件都是null或者空,如果有 custId = #{custId} 这个条件,最终的SQL,UPDATE customers SET cust_id = #{custId} WHERE custId = #{custId},所以为了避免错误产生, custId = #{custId} 仍有保留的必要。

# 5.5 trim

where 和 set 标签的功能都可以用 trim 标签来实现,并且在底层就是通过 TrimSqlNode 实现的。

where 标签对应 trim 的实现如下:

<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
1
2
3

AND 和 OR 后面的空格不能省略,为了避免匹配到 andes、orders 等词。
实际的 prefixOverrides 包含 "AND"、"OR"、"AND\n"、"OR\n"、"AND\r"、"OR\r"、"AND\t"、"OR\t"

set 标签对应 trim 的实现如下:

<trim prefix="SET" suffixOverrides=",">
...
</trim>
1
2
3

trim 标签属性:

  • prefix:给内容增加prefix指定的前缀;
  • prefixOverrides:把内容中匹配的前缀字符串去掉;
  • suffix:给内容增加suffix指定的后缀;
  • suffixOverrides:把内容中匹配的后缀字符串去掉。

# 5.6 foreach

SQL 语句中使用 IN 关键字,如 id in (1,2,3)。可使用 ${ids} 方式直接取值,但这种写法不能防止SQL注入,想避免SQL注入就需要用 #{},这时就需要配合使用 foreach 标签。

foreach 可处理数组、Map、实现Iterable接口的对象。数组在处理时会转换为List对象,所以foreach 遍历对象分两大类,Iterator类型和Map类型

<select id="selectByIds" resultMap="BaseResultMap">
    SELECT <include refid="Base_Column_List"/>
    FROM customers
    WHERE cust_id IN
    <foreach collection="list" open="(" close=")" separator="," item="custId" index="i">
    	#{custId}
    </foreach>
</select>
1
2
3
4
5
6
7
8

CustomersMapper.java

List<Customers> selectByIds(List<Integer> ids);
1

测试

@Test
public void testSelectByIds() {
    List<Integer> list = IntStream.range(10001, 10010).boxed().collect(Collectors.toList());
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
        CustomersMapper mapper = sqlSession.getMapper(CustomersMapper.class);
        System.out.println(mapper.selectByIds(list).size());
    }
}
1
2
3
4
5
6
7
8

foreach 包含以下属性:

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

DefaultSqlSession源码























 








 



package org.apache.ibatis.session.defaults;

public class DefaultSqlSession implements SqlSession {

    @Override
    public <E> List<E> selectList(String statement) {
    	return this.selectList(statement, null);
    }

    @Override
    public <E> List<E> selectList(String statement, Object parameter) {
    	return this.selectList(statement, parameter, RowBounds.DEFAULT);
    }

    @Override
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    	return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
    }

    private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
        try {
            MappedStatement ms = configuration.getMappedStatement(statement);
            return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
        } catch (Exception e) {
        	throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
        	ErrorContext.instance().reset();
        }
    }

    private Object wrapCollection(final Object object) {
    	return ParamNameResolver.wrapToMapIfCollection(object, null);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

ParamNameResolver源码

package org.apache.ibatis.reflection;

public class ParamNameResolver {

  /**
   * 如果对象是 Collection 或 数组,则包装到 ParamMap
   *
   * @param object parameter 对象
   * @param actualParamName 实际参数名称(如果指定名称,则将对象设置为具有指定名称的 ParamMap)
   * @return ParamMap 对象
   * @since 3.5.5
   */
    public static Object wrapToMapIfCollection(Object object, String actualParamName) {
        if (object instanceof Collection) {
            ParamMap<Object> map = new ParamMap<>();
            map.put("collection", object);
            if (object instanceof List) {
            	map.put("list", object);
            }
        	Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
        	return map;
        } else if (object != null && object.getClass().isArray()) {
            ParamMap<Object> map = new ParamMap<>();
            map.put("array", object);
            Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
            return map;
        }
        return object;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  • 当类型为集合时,会转换 ParamMap 类型,并添加一个 key 为 collection 的值;
  • 当类型为 List 时,除了添加 collection 这个key,还会添加一个 key 为 list,所以 XML 中 collection="list" 时,就能得到这个集合,并对他进行循环操作;
  • 当类型为数组时,也会转换 ParamMap,默认添加一个 array 的 key,所以 XML 中可以写为 collection="array"
  • actualParamName 这个值,是由Mapper中手动使用 @Param 指定的名称,所以一旦使用注解,XML中就可以使用一样的名称与之对应。

批量插入





 



<insert id="insertList">
    INSERT INTO customers (cust_name, cust_address)
    VALUES
    <foreach collection="list" item="ct" separator=",">
	    (#{ct.custName}, #{ct.custAddress})
    </foreach>
</insert>
1
2
3
4
5
6
7

通过 item 指定了循环变量名后,在引用值的时候使用的是 "属性.属性" 的方式,如 ct.custName

int insertList(List<Customers> customersList);
1

测试

@Test
public void testInsertList() {
    List<Customers> list = IntStream.range(10300, 10305).mapToObj(Customers::new).collect(Collectors.toList());
    SqlSession sqlSession = null;
    try {
        sqlSession = sqlSessionFactory.openSession();
        CustomersMapper mapper = sqlSession.getMapper(CustomersMapper.class);
        System.out.println(mapper.insertList(list));
    } finally {
        assert sqlSession != null;
        sqlSession.commit();
        sqlSession.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

返回自增主键值

  1. MyBatis 3.3.1 版本(含)开始,支持批量新增回写主键值的功能;
  2. 仅支持Mysql
<insert id="insertList" useGeneratedKeys="true" keyProperty="custId">
... ...
</insert>
1
2
3

和单次插入一样,使用 useGeneratedKeyskeyProperty 两个属性。

动态UPDATE

当参数是Map类型的时候,foreach标签的index属性值对应的不是索引值,而是Map中的key,利用这个key可以实现动态UPDATE。

<update id="updateByMap">
    UPDATE customers
    SET 
    <foreach collection="_parameter" item="val" index="key" separator=",">
    	${key} = #{val}
    </foreach>
    WHERE cust_id = #{cust_id}
</update>
1
2
3
4
5
6
7
8
int updateByMap(Map<String, Object> map);
1

测试

@Test
public void testUpdateByMap() {
    SqlSession sqlSession = null;
    try {
        sqlSession = sqlSessionFactory.openSession();
        CustomersMapper mapper = sqlSession.getMapper(CustomersMapper.class);
        Map<String, Object> map = new HashMap<>();
        map.put("cust_id", 10043);
        map.put("cust_name", "gd gz");
        map.put("cust_address", "hz");
        // 返回值 result 是执行的 SQL 影响的行数
        int result = mapper.updateByMap(map);
        // 只更新1条数据
        Assert.assertEquals(1, result);
    } finally {
        assert sqlSession != null;
        sqlSession.commit();
        sqlSession.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 5.7 bind

bind 标签可以使用 OGNL 表达式创建一个变量并将其绑定到上下文中。例如,使用 LIKE 时,经常这样写 cust_name LIKE Concat('%', #{custName}, '%'),使用 concat 函数连接字符串,在MySQL中,这个函数支持多个参数,但在Oracle中只支持两个参数。由于不同数据库之间的语法差异,如果更换数据库,有些SQL语句可能就需要重写。针对这种情况,可以使用 bind 标签来避免由于更换数据库带来的一些麻烦。如下

<if test="null != custName and '' != custName">
	<bind name="custNameLike" value="'%' + custName + '%'"/>
	AND cust_name LIKE #{custNameLike}
</if>
1
2
3
4
  • name:必填,绑定到上下文的变量名;
  • value:必填,OGNL表达式。

# 六:参考文献

最后更新: 5/27/2022, 4:01:04 PM