参考
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 | <!-- Person findById(Long personId) --> |
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 | <!-- Person findById(Long personId) --> |
虽然名字随意,但是最好还是取#{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
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]
以下几种方式都可以。arg0和param1代表第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 | public interface PersonMapper { |
现在可以使用[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 | <!-- int insert(Person perosn) --> |
4.4. 传递Map
Mapper接口1
2
3public interface PersonMapper {
Person findByIdAndName(Map<String,Object> map);
}
测试类1
2
3
4
5
6
7
8
9
10
11
12
13
14
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} 就可以引用对应的value1
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 |
|
1 |
|
Mapper接口1
2
3public interface PersonMapper {
int insert(QueryVO queryVO);
}
测试方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
3public 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
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
3DEBUG [main] - ==> Preparing: SELECT * FROM person WHERE person_id = 3 AND name = ?
DEBUG [main] - ==> Parameters: 张三(String)
DEBUG [main] - <== Total: 1
#{}是经过预编译的,安全性更高,可以防止SQL注入攻击,在获取参数时,使用#{}显然要比${}好
那么${}就没有用武之地了吗?其实${}还是有应用场景的。在JDBC中,预编译仅限于SQL语句中的参数,如以下几种形式1
2SELECT * 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
4public interface PersonMapper {
// 加一个表名参数
Person findByIdAndName(@Param("personId") Long personId, @Param("name") String name, @Param("tableName") String tableName);
}
1 | <!-- Person findByIdAndName(@Param("personId") Long personId, @Param("name") String name, @Param("tableName") String tableName) --> |
运行测试方法,成功1
2
3
4
5
6
7
8
9
10
11
12
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
3DEBUG [main] - ==> Preparing: SELECT * FROM person WHERE person_id = ? AND name = ?
DEBUG [main] - ==> Parameters: 3(Long), 张三(String)
DEBUG [main] - <== Total: 1
综上可知,在支持参数预编译的位置取值,尽量使用#{};在不支持的地方,只能通过${}实现字符串拼接
6. 返回值类型
6.1. 返回简单类型
1 | public interface PersonMapper { |
resultType设置为返回值的类型1
2
3
4<!-- Long count() -->
<select id="count" resultType="java.lang.Long">
SELECT COUNT(*) FROM person
</select>
6.2. 返回POJO
1 | public interface PersonMapper { |
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
3public 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
resultType设置为map1
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
4public interface PersonMapper {
("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主要还是应用于多表联合查询