MyBatis SQL映射文件

参考

1. SQL映射文件 概述

映射文件指导着MyBatis如何进行数据库增删改查,有着非常重要的意义。

MyBatis 的真正强大在于它的映射语句,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 为聚焦于 SQL 而构建,以尽可能地为你减少麻烦

SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):

  • cache 命名空间的二级缓存配置
  • cache-ref 其他命名空间缓存配置的引用。
  • resultMap 自定义结果集映射
  • parameterMap 已废弃!iBatis老式风格的参数映射
  • sql 抽取可重用语句块。
  • insert 映射插入语句
  • update 映射更新语句
  • delete 映射删除语句
  • select 映射查询语句

2. insert、update、delete元素

insert、update、delete元素有以下共同属性

属性 描述
id 命名空间中的唯一标识符,可被用来代表这条语句。
parameterType 将要传入语句的参数的完全限定类名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器推断出具体传入语句的参数,默认值为未设置(unset)。
parameterMap 这是引用外部 parameterMap 的已经被废弃的方法。请使用内联参数映射和 parameterType 属性。
flushCache 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:true(对于 insert、update 和 delete 语句)。
timeout 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖驱动)。
statementType STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
useGeneratedKeys (仅对 insert 和 update 有用)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系数据库管理系统的自动递增字段),默认值:false。
keyProperty (仅对 insert 和 update 有用)唯一标记一个属性,MyBatis 会通过 getGeneratedKeys 的返回值或者通过 insert 语句的 selectKey 子元素设置它的键值,默认值:未设置(unset)。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
keyColumn (仅对 insert 和 update 有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像 PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。如果希望使用多个生成的列,也可以设置为逗号分隔的属性名称列表。
databaseId 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。

2.1. id属性

与Mapper方法名绑定,一个方法对应一个id。id是不允许重复的,所以Mapper不要定义重载的方法

2.2. parameterType属性

设置形参的全类名或者别名。这个属性是可选的,因为MyBatis可以通过TypeHandler推荐出形参类型,所以实际开发中,直接省略parameterType即可

1
2
3
4
5
6
7
8
9
<!-- Person findById(Long personId) -->
<select id="findById" parameterType="java.lang.Long" resultType="demo.mybatis.entity.Person">
SELECT * FROM person WHERE person_id = #{personId}
</select>

<!-- 可以直接省略parameterType -->
<select id="findById" resultType="demo.mybatis.entity.Person">
SELECT * FROM person WHERE person_id = #{personId}
</select>

2.3. timeout属性

timeout属性可以忽略。之后会用Spring事务来控制

2.4. statementType属性

可选值:

  • STATEMENT:使用原生JDBC的Statement来执行该SQL
  • PREPARED:使用原生JDBC的PreparedStatement来执行该SQL。相对Statement要更安全,而且预编译能加速执行
  • CallableStatement:使用原生JDBC的CallableStatement来执行该SQL,CallableStatement是用来执行存储过程的

statementType默认值就是PREPARED,一般都不必调整

2.5. keyColumn属性

仅对 insert 和 update 有用,而且仅对特定数据库(像 PostgreSQL)才有用到。在开发中,该属性几乎不用

2.6. databaseId属性

在数据库迁移时有用到,可参考全局配置文件中的databaseIdProvider

2.7. useGeneratedKeys和keyProperty属性

insert 声明主键生成

1
2
3
4
5
6
7
8
9
10
<!--
useGeneratedKeys: 生成主键
keyProperty: 指定哪个属性对应数据库中的主键,这样生成主键后会自动赋值给该属性
-->
<insert id="insert" useGeneratedKeys="true" keyProperty="personId">
INSERT INTO person
(name, birthday)
VALUES
(#{name}, #{birthday})
</insert>

useGeneratedKeys="true" 的原理是执行JDBC的Statement接口的getGeneratedKeys(),来获取生成的主键

3. SelectKey

执行前生成主键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<insert id="insert">
<!--
order="BEFORE": 在执行SQL之前,执行<selectKey>中的语句
resultType="long": 执行<selectKey>中的语句之后,将返回值转为Java中的Long类型
keyProperty="personId": 将返回值赋值给personId属性
-->
<selectKey order="BEFORE" resultType="long" keyProperty="personId">
SELECT MAX(person_id) + 1 FROM person
</selectKey>
INSERT INTO person
(name, birthday)
VALUES
(#{name}, #{birthday})
</insert>

执行后生成主键 SELECT LAST_INSERT_ID(),效果和直接用useGeneratedKeys是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<insert id="insert">
<!--
order="AFTER": 在执行SQL之后,执行<selectKey>中的语句
resultType="long": 执行<selectKey>中的语句之后,将返回值转为Java中的Long类型
keyProperty="personId": 将返回值赋值给personId属性
-->
<selectKey order="AFTER" resultType="long" keyProperty="personId">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO person
(name, birthday)
VALUES
(#{name}, #{birthday})
</insert>

4. 获取传递的参数

4.1. 传递单个简单类型

传递单个简单类型参数时,#{}内部可以填写任意名称,如#{hello}#{value}等等,都可以引用参数。

1
2
3
4
<!-- Person findById(Long personId) -->
<select id="findById" resultType="demo.mybatis.entity.Person">
SELECT * FROM person WHERE person_id = #{abc}
</select>

虽然名字随意,但是最好还是取#{value}#{参数名},这样可读性更好一点

4.2. 传递多个简单类型

直接用参数名引用会出错

1
2
3
4
<!-- Person findByIdAndName(Long personId, String name) -->
<select id="findByIdAndName" resultType="demo.mybatis.entity.Person">
SELECT * FROM person WHERE person_id = #{personId} AND name = #{name}
</select>

运行测试方法

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);
Person person = mapper.findByIdAndName(3L, "张三");
System.out.println(person);
} finally {
sqlSession.close();
}
}

会出错。提示你只能通过[arg1, arg0, param1, param2]这几个参数来引用。注意老版MyBatis会提示[0, 1, param1, param2]这几个参数,现在高版本已经发生变化

1
Cause: org.apache.ibatis.binding.BindingException: Parameter 'personId' not found. Available parameters are [arg1, arg0, param1, param2]

以下几种方式都可以。arg0param1代表第1个参数,之后的类推

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- Person findByIdAndName(Long personId, String name) -->
<select id="findByIdAndName" resultType="demo.mybatis.entity.Person">
SELECT * FROM person WHERE person_id = #{arg0} AND name = #{arg1}
</select>

<!-- Person findByIdAndName(Long personId, String name) -->
<select id="findByIdAndName" resultType="demo.mybatis.entity.Person">
SELECT * FROM person WHERE person_id = #{param1} AND name = #{param2}
</select>

<!-- Person findByIdAndName(Long personId, String name) -->
<select id="findByIdAndName" resultType="demo.mybatis.entity.Person">
SELECT * FROM person WHERE person_id = #{param1} AND name = #{arg1}
</select>

但是上述方式可读性不高。此时可以结合MyBatis的@Param注解,给参数命名。实例开发时也推荐用这种方式

1
2
3
public interface PersonMapper {
Person findByIdAndName(@Param("personId") Long personId, @Param("name") String name);
}

现在可以使用[personId, name, param1, param2]来引用对应的参数

1
2
3
4
5
6
7
8
9
<!-- Person findByIdAndName(Long personId, String name) -->
<select id="findByIdAndName" resultType="demo.mybatis.entity.Person">
SELECT * FROM person WHERE person_id = #{personId} AND name = #{name}
</select>

<!-- Person findByIdAndName(Long personId, String name) -->
<select id="findByIdAndName" resultType="demo.mybatis.entity.Person">
SELECT * FROM person WHERE person_id = #{param1} AND name = #{arg1}
</select>

4.3. 传递POJO

直接用 #{pojo对应的属性名} 就可以获取POJO对应的属性值

1
2
3
4
5
6
7
<!-- int insert(Person perosn) -->
<insert id="insert">
INSERT INTO person
(name, birthday)
VALUES
(#{name}, #{birthday})
</insert>

4.4. 传递Map

Mapper接口

1
2
3
public interface PersonMapper {
Person findByIdAndName(Map<String,Object> map);
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);
HashMap<String, Object> map = new HashMap<>();
map.put("personId", 3L);
map.put("name", "张三");
Person person = mapper.findByIdAndName(map);
System.out.println(person);
} finally {
sqlSession.close();
}
}

直接用 #{Map中的key} 就可以引用对应的value

1
2
3
4
<!-- Person findByIdAndName(Map<String,Object> map) -->
<select id="findByIdAndName" resultType="demo.mybatis.entity.Person">
SELECT * FROM person WHERE person_id = #{personId} AND name = #{name}
</select>

4.5. 传递级联POJO

1
2
3
4
5
6
7
8
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
private Long personId;
private String name;
private Date birthday;
}
1
2
3
4
5
6
@Data
@NoArgsConstructor
@AllArgsConstructor
public class QueryVO {
private Person person;
}

Mapper接口

1
2
3
public interface PersonMapper {
int insert(QueryVO queryVO);
}

测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void insert() throws IOException {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);

Person person = new Person(null, "李四", new Date());
QueryVO queryVO = new QueryVO(person);
int result = mapper.insert(queryVO);
System.out.println("result = " + result);
System.out.println("person = " + person);

sqlSession.commit();
} finally {
sqlSession.close();
}
}

POJO可以直接用 #{属性名} 来得到对象。#{person}得到的是Person对象,可以继续用点操作符引用person的属性

1
2
3
4
5
6
7
<!-- int insert(QueryVO queryVO) -->
<insert id="insert">
INSERT INTO person
(name, birthday)
VALUES
(#{person.name}, #{person.birthday})
</insert>

5. #{}和${}取值的区别

在获取参数时,你会发现用#{}${}好像没有什么区别。实际上,二者的区别还是存在的。下面做一个实验

Mapper接口

1
2
3
public interface PersonMapper {
Person findByIdAndName(@Param("personId") Long personId, @Param("name") String name);
}

SQL映射文件。语句中,分别使用${personId}#{name}取值

1
2
3
4
<!-- Person findByIdAndName(@Param("personId") Long personId, @Param("name") String name) -->
<select id="findByIdAndName" resultType="demo.mybatis.entity.Person">
SELECT * FROM person WHERE person_id = ${personId} AND name = #{name}
</select>

配置好log4j输出日志,再运行以下测试方法

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);
Person person = mapper.findByIdAndName(3L, "张三");
System.out.println(person);
} finally {
sqlSession.close();
}
}

运行之后,输出SQL如下。你会发现,${person_id}是直接以字符串的形式拼接到SQL语句中,而#{name}是以预编译占位符的方式,在预编译之后将值填充进去。其实这就是二者的区别

1
2
3
DEBUG [main] - ==>  Preparing: SELECT * FROM person WHERE person_id = 3 AND name = ? 
DEBUG [main] - ==> Parameters: 张三(String)
DEBUG [main] - <== Total: 1

#{}是经过预编译的,安全性更高,可以防止SQL注入攻击,在获取参数时,使用#{}显然要比${}

那么${}就没有用武之地了吗?其实${}还是有应用场景的。在JDBC中,预编译仅限于SQL语句中的参数,如以下几种形式

1
2
SELECT * FROM person WHERE name = ?
SELECT * FROM person WHERE id = ? AND name = ?

但是不能对其它位置进行预编译,如表名

1
2
-- ERROR,不能对表名进行预编译
SELECT * FROM ? WHERE name = "jack"

也就是説,MyBatis不能写以下语句

1
2
3
4
<!-- ERROR,因为#{tableName}表名不能预编译 -->
<select id="findByIdAndName" resultType="demo.mybatis.entity.Person">
SELECT * FROM #{tableName} WHERE person_id = #{personId} AND name = #{name}
</select>

此时,就可以用${},传入表名,进行字符串动态拼接

1
2
3
4
public interface PersonMapper {
// 加一个表名参数
Person findByIdAndName(@Param("personId") Long personId, @Param("name") String name, @Param("tableName") String tableName);
}

1
2
3
4
<!-- Person findByIdAndName(@Param("personId") Long personId, @Param("name") String name, @Param("tableName") String tableName) -->
<select id="findByIdAndName" resultType="demo.mybatis.entity.Person">
SELECT * FROM ${tableName} WHERE person_id = #{personId} AND name = #{name}
</select>

运行测试方法,成功

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);
// 第3个参数代表person表
Person person = mapper.findByIdAndName(3L, "张三", "person");
System.out.println(person);
} finally {
sqlSession.close();
}
}

这是日志输出

1
2
3
DEBUG [main] - ==>  Preparing: SELECT * FROM person WHERE person_id = ? AND name = ? 
DEBUG [main] - ==> Parameters: 3(Long), 张三(String)
DEBUG [main] - <== Total: 1

综上可知,在支持参数预编译的位置取值,尽量使用#{};在不支持的地方,只能通过${}实现字符串拼接

6. 返回值类型

6.1. 返回简单类型

1
2
3
public interface PersonMapper {
Long count();
}

resultType设置为返回值的类型

1
2
3
4
<!-- Long count() -->
<select id="count" resultType="java.lang.Long">
SELECT COUNT(*) FROM person
</select>

6.2. 返回POJO

1
2
3
public interface PersonMapper {
Person findById(Long personId);
}

resultType设置为返回值的类型

1
2
3
4
<!-- Person findById(Long personId) -->
<select id="findById" resultType="demo.mybatis.entity.Person">
SELECT * FROM person WHERE person_id = #{personId}
</select>

6.3. 返回List

Mapper接口

1
2
3
public interface PersonMapper {
List<Person> listPerson();
}

映射文件。返回List,resultType设置为List的元素类型即可,MyBatis会自动判断,封装为List并返回

1
2
3
4
<!-- List<Person> listPerson() -->
<select id="listPerson" resultType="demo.mybatis.entity.Person">
SELECT * FROM person
</select>

6.4. 返回Map单条记录

返回单条记录时,可以用Map封装。key代表列名,value代表值

resultType设置为map

1
2
3
4
<!-- Map<String, Object> findById(Long personId) -->
<select id="findById" resultType="map">
SELECT * FROM person WHERE person_id = #{value}
</select>

6.5. 返回Map多条记录

返回多条记录时,可以用Map<主键类型, POJO>,每一个key/value都代表一行记录

首先,要用@MapKey注解,声明Map的key要封装哪个属性

1
2
3
4
public interface PersonMapper {
@MapKey("personId")
Map<Long, Person> listPerson();
}

resultType设置为POJO,即value的类型

1
2
3
<select id="listPerson" resultType="demo.mybatis.entity.Person">
SELECT * FROM person
</select>

6.6. resultMap自定义封装类型

当查询返回的列名与POJO属性名不一致时,就需要用resultMap自定义封装类型

但是有几种情况是可以避免使用resultMap的

  • 下划线命名法与驼峰命名法不一致,这种情况可以直接用MyBatis自带的mapUnderscoreToCamelCase,不必使用resultMap
  • 可以用SQL的AS对列名重命名,使得与POJO属性名一致,这样就不必使用resultMap了。

resultMap主要还是应用于多表联合查询

panchaoxin wechat
关注我的公众号
支持一下