目录

  1. XML 映射器
  2. <select>
    1. 常见配置
    2. 底层设计
    3. 属性
  3. insert,update&delete
    1. 获取自增主键的值
    2. 获取非自增主键的值
  4. 参数处理
    1. 单个参数
    2. 多个参数
    3. 命名参数
    4. POJO
    5. Map
    6. TO
    7. Demo
  5. 字符串替换
  6. 结果映射
    1. <resultMap>
    2. 场景一:联合查询
    3. 场景一:分步查询
    4. 场景二:联合查询
    5. 场景二:分步查询
    6. discriminator(鉴别器)

XML 映射器

MyBatis 的真正强大在于它的语句映射,SQL 映射文件只有很少的几个顶级元素(根据应被定义的顺序列出):

标签含义
cache该命名空间的缓存配置
cache-ref引用其他命令空间的缓存配置
resultMap描述如何从数据库结果集中加载对象,时最复杂也是最强大的元素
sql可被其它语句引用的可重用语句块
insert映射插入语句
update映射更新语句
delete映射删除语句
select映射查询语句

<select>

多数应用都是查询密集型,所以先从查询开始

🎶 MyBatis 的基本原则之一是:在每个插入、更新或删除操作之间,通常会执行多个查询操作

常见配置

1
2
3
<select id="selectPerson" parameterType="int" resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>

✨语句特点

  1. 这个语句名是selectPerson
  2. 接受一个int(Integer)类型的参数
  3. 返回一个HashMap类型的对象,其中键是列名,值便是结果行中的对应值
  4. 参数使用#{}进行获取

底层设计

以上的查询语句,其实就是告诉 MyBatis 创建一个预处理语句(PreparedStatement)参数

1
2
3
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);

属性

select 元素可以配置很多属性来配置每条语句的行为细节

1
2
3
4
5
6
7
8
9
10
11
12
<select
id="selectPerson"
parameterType="int"
parameterMap="deprecated"
resultType="hashmap"
resultMap="personResultMap"
flushCache="false"
useCache="true"
timeout="10"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD_ONLY">
属性描述
resultType期望从这条语句中返回结果的类全限定名或别名。 注意,如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 resultType 和 resultMap 之间只能同时使用一个。

insert,update&delete

数据变更语句 insert,update 和 delete 的实现非常接近:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<insert
id="insertAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
keyProperty=""
keyColumn=""
useGeneratedKeys=""
timeout="20">

<update
id="updateAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">

<delete
id="deleteAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">

获取自增主键的值

mysql 支持自增主键,自增主键值的获取,mybatis 利用statement.getGeneratedKeys()

useGenerateKeys="true"使用自增主键获取主键值策略

keyProperty指定对应的主键属性,也就是 mybatis 获取到主键值以后,将这个值封装给 javabean 的那个属性

属性描述
databaseId如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。
1
2
3
4
5
6
<!-- public void addEmp(Employee employee); -->
<insert id="addEmp" parameterType="com.atguigu.mybatis.bean.Employee"
useGeneratedKeys="true" keyProperty="id" databaseId="mysql">
insert into tbl_employee(last_name,email,gender)
values(#{lastName},#{email},#{gender})
</insert>

获取非自增主键的值

Oracle 不支持自增使的数据库,使用 selectKey 子元素

✨selectKey 元素将会首先运行, id 会被设置, 然后插入语句会被调用

属性描述
keyProperty仅适用于 insert 和 update,指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)。如果生成列不止一个,可以用逗号分隔多个属性名称。
order“BEFORE”:当前 sql 在插入 sql 之前运行
“AFTER”:当前 sql 在插入 sql 之后运行

🎶BEFORE 与 AFTER 的运行顺序

  • BEFORE:
    • 先运行 selectKey 查询 id 的 sql;查出 id 值封装给 javaBean 的 id 属性
    • 再运行插入的 sql;就可以取出 id 属性对应的值
  • AFTER
    • 先运行插入的 sql(从序列中取出新值作为 id);
    • 再运行 selectKey 查询 id 的 sql;
1
2
3
4
5
6
7
8
<insert id="addEmp" databaseId="oracle">
<selectKey keyProperty="id" order="BEFORE" resultType="Integer">
select EMPLOYEES_SEQ.nextval from dual
</selectKey>
<!-- 插入时的主键是从序列中拿到的 -->
insert into employees(EMPLOYEE_ID,LAST_NAME,EMAIL)
values(#{id},#{lastName},#{email,jdbcType=NULL})
</insert>

参数处理

DAO 和 MyBatis Mapper 之间参数传递的处理

单个参数

可以接受基本类型, 对象类型。 这种情况 MyBatis 可直接使用这个参数, 不需要经过任何处理

1
#{参数名/任意名}

多个参数

任意多个参数, 都会被 MyBatis 重新包装成一个 Map 传入

✨Map 的 key 是 param1,param2, 或者 0, 1…, 值就是参数的值

1
2
3
key: param1 .. paramN
value:传入的参数值
#{param1}就是从map中获取指定的key的值

命名参数

🎶明确指定封装参数时 map 的 key @Param("id")

1
2
3
key:使用@Param注解指定的值
value:参数值
#{指定的key}取出对应的参数值

POJO

如果多个参数正好是我们业务逻辑的数据模型,我们就可以直接传入 POJO

1
#{属性名}:取出传入的pojo的属性值

Map

如果多个参数不是业务模型中的数据,没有对应的 pojo,不经常使用,为了方便,我们也可以传入 map

1
#{key}:取出map中对应的值

TO

如果多个参数不是业务模型中的数据,但是经常要使用,推荐编写一个 TO(Transfer Object)数据传输对象

1
2
3
4
Page{
int index;
int size;
}

Demo

1
2
3
4
5
6
public Employee getEmp(@Param("id")Integer id,String lastName);
id ==> #{id}
lastName ==> #{param2}
public Employee getEmp(Integer id,@Param("e")Employee emp);
id ==> #{id}
lastName ==> #{param2.lastName}|#{e.lastName}

🎶如果是Collection(List,Set)类型或者数组,也会特殊处理,也是把传入的list或者数组封装在map

类型key
Collectioncollection
Listlist
数组array
1
2
public Employee getEmpById(List<Integer> ids);
//取出第一个 id 的值:#{list[0]}

🎶参数多时会封装 map,为了不混乱,我们可以使用@Param来指定封装时使用的key

对于大多数简单的使用场景,不需要使用复杂的参数

1
2
3
4
5
<select id="selectUsers" resultType="User">
select id, username, password
from users
where id = #{id}
</select>

根据参数类型(parameterType)会被自动设置为int,这个参数可以随意命名

原始类型或简单数据类型(比如IntegerString)因为没有其他属性,会用它们的值来作为参数,然而,如果传入一个复杂的对象,行为就会不一样了

1
2
3
4
<insert id="insertUser" parameterType="User">
insert into users (id, username, password)
values (#{id}, #{username}, #{password})
</insert>

如果 User 类型的参数对象传递到了语句中,会查找 id、username 和 password 属性,然后将它们的值传入预处理语句的参数中

参数也可以指定一个特殊的数据类型

1
#{property,javaType=int,jdbcType=NUMERIC}

几乎总是可以根据参数对象的类型确定 javaType,除非该对象是一个 HashMap。这个时候,你需要显式指定 javaType 来确保正确的类型处理器(TypeHandler)被使用。

🎶JDBC 要求,如果一个列允许使用 null 值,并且会使用值为 null 的参数,就必须要指定 JDBC 类型(jdbcType)

要更进一步地自定义类型处理方式,可以指定一个特殊的类型处理器类(或别名)

1
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}

对于数值类型,还可以设置 numericScale 指定小数点后保留的位数

1
#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}

👴参数的配置好像越来越繁琐了,但实际上,很少需要如此繁琐的配置

mode 属性允许你指定 INOUTINOUT 参数。如果参数的 modeOUTINOUT,将会修改参数对象的属性值,以便作为输出参数返回。 如果 modeOUT(或 INOUT),而且 jdbcTypeCURSOR(也就是 Oracle 的 REFCURSOR),你必须指定一个 resultMap 引用来将结果集 ResultMap 映射到参数的类型上。要注意这里的 javaType 属性是可选的,如果留空并且 jdbcType 是 CURSOR,它会被自动地被设为 ResultMap

1
#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}

🎶大多时候,你只须简单指定属性名,顶多要为可能为空的列指定 jdbcType,其他的事情交给 MyBatis 自己去推断就行了

1
2
3
#{firstName}
#{middleInitial,jdbcType=VARCHAR}
#{lastName}

字符串替换

表达式含义
#{}可以获取 map 中的值或者 pojo 对象属性的值;
是以预编译的形式,将参数设置到sql语句中;
使用PreparedStatement;防止 sql 注入
${}可以获取 map 中的值或者 pojo 对象属性的值;
取出的值直接拼接在sql语句中;
存在安全问题

原生 jdbc 不支持占位符的地方我们就可以使用${}进行取值,如:分表、排序

当 SQL 语句中的元数据(如表名或列名)是动态生成的时候,字符串替换将会非常有用。 举个例子,如果你想 select 一个表任意一列的数据时,不需要这样写:

1
2
3
4
5
6
7
8
9
10
@Select("select * from user where id = #{id}")
User findById(@Param("id") long id);

@Select("select * from user where name = #{name}")
User findByName(@Param("name") String name);

@Select("select * from user where email = #{email}")
User findByEmail(@Param("email") String email);

// 其它的 "findByXxx" 方法

而是可以只写这样一个方法:

1
2
@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);

其中 ${column} 会被直接替换,而 #{value} 会使用 ? 预处理。 这样,就能完成同样的任务:

1
2
3
User userOfId1 = userMapper.findByColumn("id", 1L);
User userOfNameKid = userMapper.findByColumn("name", "kid");
User userOfEmail = userMapper.findByColumn("email", "noone@nowhere.com");

这种方式也同样适用于替换表名的情况

🎶用这种方式接受用户的输入,并用作语句参数是不安全的,会导致潜在的 SQL 注入攻击,因此,要么不允许用户输入这些字段,要么自行转义并检验这些参数

👴大多数情况下,我们取参数的值都应该是使用#{}

结果映射

ResultMap 的设计思想是:对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之前的关系就可以了

如果返回的是一个集合,要写集合中元素的类型

1
2
@MapKey("lastName")
public Map<String, Employee> getEmpByLastNameLikeReturnMap(String lastName);

@Mapkey告诉 mybatis 封装这个 map 的时候使用那个属性作为 map 的 key

返回一条记录的 map,key 是列名,值是对应的值

1
public Map<String, Object> getEmpByIdReturnMap(Integer id);

<resultMap>

自定义结果集映射规则,实现高级结果集映射

标签含义
<id>定义主键列封装规则,底层有优化
<result>定义普通列封装规则
1
2
3
4
5
6
7
8
9
<resultMap type="com.atguigu.mybatis.bean.Employee" id="MySimpleEmp">
<!--column:指定哪一列 property:指定对应的javaBean属性-->
<!--定义主键列封装规则-->
<id column="id" property="id"/>
<!-- 定义普通列封装规则 -->
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
</resultMap>

👴其他不指定的列会自动封装;只要写 resultMap 就把全部的映射规则都写上

各个属性含义信息

属性含义
property映射到 Java Bean 的属性
column数据表的列命
javaType一个 Java 类的完全限定名,或一个类型别名。如果映射到一个 Java Bean,MyBatis 通常可以断定类型
jdbcTypeJDBC 类型是仅仅需要对插入,更新和删除操作可能为空的列进行处理
typeHandler类型处理器,使用这个属性,可以覆盖默认的类型处理器,这个属性值是类的完全限定名或者是一个类型处理器的实现,或者是类型别名

场景一:联合查询

查询 Employee 的同时查询员工对应的部门

1
2
3
4
5
6
7
8
<resultMap type="com.atguigu.mybatis.bean.Employee" id="MyDifEmp">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="gender" property="gender"/>
<!--级联赋值-->
<result column="did" property="dept.id"/>
<result column="dept_name" property="dept.departmentName"/>
</resultMap>

使用<association>定义关联的单个对象的封装规则,association 指定联合的 javaBean 对象

1
2
3
4
5
<!--property指定那个属性是联合对象,javaType:指定这个属性对象的类型-->
<association property="dept" javaType="com.atguigu.mybatis.bean.Department">
<id column="did" property="id"/>
<result column="dept_name" property="departmentName"/>
</association>

场景一:分步查询

使用 association 进行分布查询

  1. 先按照员工 id 查询员工信息
  2. 根据查询员工信息中的 d_id 值去部门表查出部门信息
  3. 部门信息设置到员工中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<result column="gender" property="gender"/>
<!-- association定义关联对象的封装规则
select:表明当前属性是调用select指定的方法查出的结果
column:指定将哪一列的值传给这个方法

流程:使用select指定的方法(传入column指定的这列参数的值)查出对象,并封装给property指定的属性
-->
<association property="dept"
select="com.atguigu.mybatis.dao.DepartmentMapper.getDeptById"
column="d_id">
</association>
</resultMap>
<!-- public Employee getEmpByIdStep(Integer id);-->
<select id="getEmpByIdStep" resultMap="MyEmpByStep">
select * from tbl_employee where id=#{id}
</select>

分段查询的基础之上加上两个配置,可以实现延迟加载(懒加载、按需加载)

1
2
3
4
5
6
<settings>
<setting name="jdbcTypeForNull" value="NULL"/>
<!--显示的指定每个我们需要更改的配置的值,即使他是默认的。防止版本更新带来的问题 -->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>

场景二:联合查询

查询部门的时候将部门对应的所有员工信息也查询出来

1
2
3
4
5
6
public class Department {

private Integer id;
private String departmentName;
private List<Employee> emps;
}
1
2
3
4
5
6
7
8
9
@Alias("emp")
public class Employee {

private Integer id;
private String lastName;
private String email;
private String gender;
private Department dept;
}
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
<!--嵌套结果集的方式,使用collection标签定义关联的集合类型的属性封装规则  -->
<resultMap type="com.atguigu.mybatis.bean.Department" id="MyDept">
<id column="did" property="id"/>
<result column="dept_name" property="departmentName"/>
<!--
collection定义关联集合类型的属性的封装规则
ofType:指定集合里面元素的类型
-->
<collection property="emps" ofType="com.atguigu.mybatis.bean.Employee">
<!-- 定义这个集合中元素的封装规则 -->
<id column="eid" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
</collection>
</resultMap>
<!-- public Department getDeptByIdPlus(Integer id); -->
<select id="getDeptByIdPlus" resultMap="MyDept">
SELECT d.id did,d.dept_name dept_name,
e.id eid,e.last_name last_name,e.email email,e.gender gender
FROM tbl_dept d
LEFT JOIN tbl_employee e
ON d.id=e.d_id
WHERE d.id=#{id}
</select>

场景二:分步查询

1
2
3
4
5
6
7
8
9
10
11
<resultMap type="com.atguigu.mybatis.bean.Department" id="MyDeptStep">
<id column="id" property="id"/>
<id column="dept_name" property="departmentName"/>
<collection property="emps"
select="com.atguigu.mybatis.dao.EmployeeMapperPlus.getEmpsByDeptId"
column="{deptId=id}" fetchType="lazy"></collection>
</resultMap>
<!-- public Department getDeptByIdStep(Integer id); -->
<select id="getDeptByIdStep" resultMap="MyDeptStep">
select id,dept_name from tbl_dept where id=#{id}
</select>

📖多列的值传递过去:将多列的值封装 map 传递;column=“{key1=column1,key2=column2}”
fetchType=“lazy”:表示使用延迟加载;- lazy:延迟- eager:立即

discriminator(鉴别器)

mybatis 可以使用 discriminator 判断某列的值,然后根据某列的值改变封装行为

封装 Employee:

  • 如果查出的是女生:就把部门信息查询出来,否则不查询;
  • 如果是男生,把 last_name 这一列的值赋值给 email;
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
<resultMap type="com.atguigu.mybatis.bean.Employee" id="MyEmpDis">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
<!--
column:指定判定的列名
javaType:列值对应的java类型 -->
<discriminator javaType="string" column="gender">
<!--女生 resultType:指定封装的结果类型;不能缺少。/resultMap-->
<case value="0" resultType="com.atguigu.mybatis.bean.Employee">
<association property="dept"
select="com.atguigu.mybatis.dao.DepartmentMapper.getDeptById"
column="d_id">
</association>
</case>
<!--男生 ;如果是男生,把last_name这一列的值赋值给email; -->
<case value="1" resultType="com.atguigu.mybatis.bean.Employee">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="last_name" property="email"/>
<result column="gender" property="gender"/>
</case>
</discriminator>
</resultMap>