MyBatis

ORM(Object Relational Mapping)


ORM思想:为了解决面向对象与关系数据库存在的互不匹配的现象的技术。ORM是通过使用描述对象和数据库之间映射的元数据,将java程序中的对象自动持久化到关系数据库中。将关系数据库中表中的记录映射成对象,以对象的形式展现。把对数据库的操作转化为对对象的操作。因此 ORM 的目的是为了让开发人员以面向对象的思想来实现对数据库的操作。

目前流行的 ORM 框架:

  1. JPA : 一种 ORM 规范,需要各大 ORM 框架实现,JavaEE 的规范,Java persistence api : Java 持久化 API
  2. Hibernate : 最流行的 ORM 框架,设计灵巧,性能优秀,文档丰富
  3. MyBatis : 本是 apache 的一个开源项目 iBatis,提供的持久层框架包括SQL、Maps和DAO,允许开发人员直接编写SQL

mybatis 概述


MyBatis 是一个 SQL 的映射框架,其前身是 iBatis,也就是淘宝试用的持久层框架,org.apache.ibatis

mybatis 主要功能

几乎消除了所有 JDBC 代码和参数的手工设置以及结果集都可以交给 MyBatis完成

这些只需要简单的使用 XML 或者 注释配置即可

使用步骤


  1. 使用配置文件(主配置文件/总配置文件/全局配置文件),告知框架连接数据库四要素
  2. 使用配置文件(SQL映射文件),告知框架执行哪些SQL
  3. 启动框架,让框架执行具体SQL任务

框架从对应的配置文件中获取数据(DOM 解析),其他的事情都已经做好了。

准备工作


导入 mybatis


导入 mybatis 核心包和 mysql 驱动包

创建数据库表


创建domain类


MyBatis-config.XML


又叫 主配置文件/总配置文件/全局配置文件

主要是告知框架 连接数据库四要素,事务处理,关联mapper文件,启动框架时,只会加载主配置文件

规范

  1. 起名:mybatis-config.xml
  2. 路径,放在classpath 下面,创建一个 source folder:resources
  3. 参考文档:mybatis 中文文档/XML 映射文件

约束头


1
2
3
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">

约束头的作用:有网络会用网络上的 dtd约束, 如果没有网络,需要配置离线的约束,离线约束加载如下,只需将 dtd 放在 resources 即可

MyBatis-config.XML


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
35
36
37
<?xml version="1.0" encoding="UTF-8" ?> 
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
<!-- 这句xml 的作用:将db.properties 属性装包:为了可以用#{}去取值 -->
<properties resource="db.properties"></properties>
<!-- 别名,主配置文件,配好,mapper 可以使用 -->
<!-- 别名,主配置文件,配好,mapper 可以使用 -->
<typeAliases>
<!-- <typeAlias type="cn.lizhaoloveit.mybatis.User" alias="User"/> -->
<!-- 给包取别名后,默认别名是简单名称 类型的别名,只能在 Type 中使用 -->
<package name="cn.lizhaoloveit.mybatis"/>
</typeAliases>
<!-- 配置多个环境 default:默认使用什么环境(开发,测试,上线等) -->
<environments default="dev">
<!-- id:当前环境的唯一标记,不能重复告诉框架事务处理和四要素 -->
<environment id="dev">
<!-- 事务处理的类型,使用 JDBC 回滚,提交,表示要使用事务 -->
<transactionManager type="JDBC"></transactionManager>
<!-- type:连接池的类型,POOLED 表示可以使用连接池 -->
<dataSource type="POOLED">
<property name="driver" value="${driverClassName}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!-- 启动框架的时候,只会加载主配置文件,不会加载映射文件,
使用 mappers 标签,告诉框架上哪找 SQL 映射文件 -->
<mappers>
<!-- resource 映射文件的路径,以斜杠分割,从classpath 开始找 -->
<mapper resource="cn/lizhaoloveit/mybatis/UserMapper.xml"/>
</mappers>
</configuration>

MyBatis 的 XML 配置文件的高级结构如下:

  • configuration 配置
    • properties 属性
    • setting 设置
    • typeAliases 类型命名
    • typeHandlers 类型处理器
    • objectFactory 对象工厂
    • plugins 插件
    • environments 环境
      • environment 环境变量
        • transactionManager 事务管理器
        • dataSource 数据源
    • databaseIdProvider chinese?
    • mappers 映射器

mapper映射文件


主要告知框架可以执行哪些数据库操作(写增删改查的SQL语句)

规范

  1. 起名:xxxMapper.xml -> xxx 表示 domain, userMapper.xml
  2. 路径:映射文件跟 mapper(用来做增删改查) 接口 位置一致。

约束头


1
2
3
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

标签属性(自动返回主键)


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

  • cache 配置给定命名空间的缓存
  • cache-ref 从其他命名空间引用缓存配置
  • resultMap 最复杂,用来描述如何从数据库结果集中加载你的对象
  • sql 可以重用的 SQL 块,
  • insert 映射插入语句
  • update 映射更新语句
  • delete 映射删除语句
  • select 映射查询语句
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
<!-- namespace: 命名空间,给当前 mapper 文件取一个名字,
找到指定的 SQL 语句: namespace 值 + . + id值
namespace 的值需要规范,使用 mapper 接口的全限定名 -->
<mapper namespace="cn.wolfcode.UserMapper">
<!-- SQL语句写在标签中间
id:当前 SQL 语句的唯一标记,id 值跟接口中的方法名一致
注意:mapper 文件中,不认识占位符,使用语法 #(属性名),从对象中
取什么属性的值放在占位符上,替代占位符 ? useGenerateKeys 表示是否
要获取自动生成主键,keyProperty 表示把主键赋值到哪个属性上 -->
<insert id="save" useGeneratedKeys="true" keyProperty="id">
inset into user(name, salary, hiredate) values(#{name}, #{salary}, #{hiredate})
</insert>
<update id="update">
update user set name=#{name}, salary=#{salary}, hiredate=#{hiredate} where id=#{id};
</update>
<delete id="delete">
delete from user where id=#{id};
</delete>
<select id="get" resultType="User">
select * from user where id=#{id};
</select>
<select id="gets" resultType="User">
select * from user;
</select>
</mapper>

建议:写 SQL 的时候,先写带有占位符 ? 的那种写法,再使用 #{} 去替换。

resultMap


如果列名和属性名匹配,可以直接使用 resultType,否则就要使用 resultMap了

只要是查询:就一定要有结果类型, 要么是 resultType, 要么是 resultMap

封装映射,将字段名和属性名一一对应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- resultType 属性:如果列名和属性名匹配,可以使用 
如果不匹配,用 resultMap
resultMap: 要去匹配哪一个 resultMap 标签
-->
<select id="listAll" resultMap="basemap">

</select>

<!-- type: 跟 java 中的类型做映射
id: 当前 resultMap 标签的唯一映射
-->
<resultMap type="cn.lizhaoloveit.User" id="basemap">
<!-- id 用来处理主键 -->
<id/ cloumn="uid" property="id"/>
<result column="uname" property="name"/>
<result column="usalary" property="salary"/>
<result column="uhiredate" property="hiredate"/>
</resultMap>

只要是查询,一定有结果类型。

使用 mapper 接口


  1. 创建一个 Mapper 接口,这个接口的要求:
    1. 这个接口的全限定名===对应的 Mapper 文件的 namespace
    2. 这个接口中的方法和 Mapper 文件的 SQL 元素一一对应:方法名字=SQL元素id,方法的参数类型=SQL元素中定义的 parameterType 类型,方法的返回类型=SQL元素中定义的resultType/resultMap 类型’
  2. 创建 sqlSession
  3. 通过 sqlSession.getMapper(xxxMapper.class) 方法得到一个 Mapper 对象
  4. 调用 Mapper 对象上的方法文成对象的查询

使用 @Param 设置多个参数


如果要在 Mapper 接口上的一个方法中,添加多个参数,就一定要在每个参数前使用 @Param 标签

User login(@Param(“name”)String name, @Param(“password”)String password);

原理:Mybatis 在处理这些Mapper方法的时候,会自动的把这些参数包装成一个 Map 对象,@Param中的 value 就会作为这个 Map 的 key,对应的参数值,就会作为这个 Key 的value

${}在mapper.xml 中的使用场景

mapper.xml select 中若需要作为 order by group by 取值,要用${} , (#{}会给列名 + '') 会导致排序无效。

外键支持


我们不能在数据库添加外键,这样会非常影响数据库读写数据的性能。所以使用代码去实现表与表之间的关联是必须的。

一对一


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<resultMap type="mybatis.Orders" id="OrdersUserResultMap">
<!-- 配置映射订单信息 -->
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="number" property="number"/>
<result column="createtime" property="createtime"/>
<result column="note" property="note"/>

<!-- 配置映射的关联的用户信息 -->
<!-- association用于映射关联查询单个对象的信息
property:要将关联查询的用户信息映射到Orders中的哪个属性
javaType:该属性的类型
select: 转换对象的 sql,即如何将 user_id转换成 User 对象
-->
<association property="user" javaType="mybatis.po.User">
<id column="user_id" property="id"/>
<result column="username" property="username"/>
<result column="sex" property="sex"/>
<result column="address" property="address"/>
</association>
</resultMap>

上述配置,相当于一个 Order 类中保存这一个 User 类,使用 标签来

多对一(多个学生与一个老师)


由 Student类来维护 Student与 Teacher的关系。

项目结构如下:

2019-07-24 at 8.50 P

表结构:

2019-07-24 at 8.47 P

domain 类

1
2
3
4
5
6
7
8
9
10
11
12
public class Student {
private Long id;
private String name;
private String password;
private Teacher teacher;
}

public class Teacher {
private Long id;
private String name;
private String sex;
}

多对一(add) 添加数据


写代码时,我一般的习惯是先从测试入手,先想好程序外面会如何使用功能,再着手实现。思考过程如下:
Student 类中包含 Teacher,所以想要得到 Student 必须先有 Teacher。Test 中代码就很容易写了

1
2
3
4
5
6
7
8
9
10
11
public void testSave() {
// Student 类中包含 Teacher ,所以想要得到 Student 必须先有 Teacher
Teacher tea = new Teacher(null, "小A", "女");
teaDao.save(tea);

Student stu1 = new Student(null, "A", "123456", tea);
Student stu2 = new Student(null, "B", "123456", tea);

stuDao.save(stu1);
stuDao.save(stu2);
}

TeacherMapper.xml

1
2
3
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into teacher_all2one (name, sex) values(#{name}, #{sex})
</insert>

StudentMapper.xml

1
2
3
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into student_all2one (name, password, tea_id) values(#{name}, #{password}, #{teacher.id})
</insert>

在保存的时候,先保存 teacher 然后根据 tea 的 id 属性,保存stu1,stu2。

多对一(select)


还是老办法,先写 test

1
2
3
4
5
@Test
public void testGet() {
Student student = stuDao.get(1l);
System.out.println(student);
}

思考:从上面的测试方法中,我们需要得到一个 student 对象,且属性 teacher 要能够被赋值。

解决方法,使用 resultMap 映射,将 teacher 属性映射成一行数据,该行数据正好是 teacher 表中的一行,即查询 teacher 表中的一行数据赋值给 Student 类中的 teacher 属性。

StudentMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<resultMap type="Student" id="studentMap">
<!-- 关联关系映射,(某一列,映射成对象),
association: 关联关系映射,映射成对象,
property: 要映射的对象属性:Student 的 teacher属性
column: 要映射表的列名: Student 表的 tea_id 列
select: 转换对象的 sql,即如何将 tea_id 转换成 teacher 对象
sql: selec * from teacher where id=#{id} 执行sql 返回对象,并赋值给 Student
的 teacher 属性
通过 column 对应列的值,执行 sql 查询出 teacher 对象
列-------类-------对象--------赋值
-->
<association property="teacher" javaType="Teacher" column="tea_id"
select="cn.lizhaoloveit.mybatis.mapper.TeacherMapper.getById">
</association>
</resultMap>

<select id="getById" resultMap="studentMap">
select * from student_all2one where id=#{id}
</select>

TeacherMapper.xml

1
2
3
<select id="getById" resultType="Teacher">
select * from teacher_all2one where id=#{id}
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<resultMap type="全限定名" id="map">
<id column="id" property="id"/>
<!-- 普通映射字段 -->
<result column="name" property="name"/>
<!-- 关联关系映射,(某一列,映射成对象),
association: 关联关系映射,映射成对象,
property: 要映射的对象属性:Employee 的 dept属性
column: 要映射表的列名: employee 表的 dept_id 列
select: 转换对象的 sql,即如何将 dept_id转换成 Department 对象
sql: selec id, name from department where id=#{id}
通过 column 对应列的值,执行 sql 查询出 department 对象

列-------类
select 类中的变量信息,from department where id=#{已知信息}
-->
<association select="全限定名.get" property="dept" column="dept_id"/>
</resultMap>

使用了继承,是因为订单信息和关联的用户信息和前面一对一是完全一样的,不需要再写一遍,resultMap 支持继承,直接继承那个 resultMap 即可,然后加上订单明细这部分。

N+1 sql (多对一查询)


问题:多对一 n+1 问题是指以下情况,假设系统中存在两张表,学生表(多),和老师表(1),
学生表字段:sid,sname,tid。老师表字段为 tid tname , 两张表通过 tid关联。

现在假设一个页面需要浏览学生记录,同时要显示这个学生对应的老师名称,有两种查询方法。

  1. n+1 句 sql,先查询 select * from 学生表。再根据结果记录查询 select * from 老师表 where tid = ?
  2. 建立一条联合查询语句 select * from 学生表 a inner join 老师表 b on a.tid=b.tid

在没有分页的情况下,第二种方法的效率比第一种方法高,然而在有分页的情况下,反而是 n+1 句 sql 的执行效率高。

没分页的情况下,复杂度 10000 + 10000 * 1000 第二个是 10000 * 1000

假设两张表没有索引,学生表有10000条记录,老师表为1000条记录,每个页面显示 100 条记录,第一种方法数据库的执行的时间复杂度是 10000 + 100 * 1000。

第二种方法是 10000 * 1000.
双方差100倍
所以系统存在分页,采取 n+1 句sql 语句进行查询的效率更高。而且第一种方法也是一种扩展性比较良好的方法,假设老师表的数据过多,我们需要把老师表进行水平切割成100个表,那么第一种方法的扩展性是比较好的。

总结:分页查询和查询单个对象时,一般使用 N + 1,在全部列表查询,一般使用多表查询

1
2
3
4
5
public class student {
private Long id;
private String name;
private Teacher teacher
}
1
2
3
4
5
6
<resultMap type="Student" id="studentMap">
<id column="id" property="id"/>
<!-- 普通映射字段 -->
<result column="name" property="name"/>
<association select="Teacher.get" property="teacher" column="t_id"/>
</resultMap>

两个 maper,一个是 select * from student, 一个是 select * from teacher where id = tid

多表联查


1
2
3
4
5
6
public interface EmployeeMapper {
void save(Employee employee) {
Employee get(Long id);
List<Employee> list();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<resultMap type="Employee" id="employeeMap">
<!-- 配置映射订单信息 -->
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="name" property="name"/>

<!-- 配置映射的关联的用户信息 -->
<!-- association用于映射关联查询单个对象的信息
property:要将关联查询的用户信息映射到Orders中的哪个属性
javaType:该属性的类型
select: 转换对象的 sql,即如何将 user_id转换成 User 对象
解释属性 teacher 的类型有哪些属性 关联 查询表的那些列
-->
<association property="dept" javaType="Department">
<id column="d_id" property="id"/>
<result column="d_name" property="name"/>
</association>
</resultMap>
<select id="get" resultMap="singleMap2">
select e.id, e.name, d.id d_id, d.name d_name from employee e left join department d on d.dept_id=d.id where e.id=#{id}
</select>
1
SELECT * FROM student LEFT JOIN teacher ON tid = id WHERE student.id = #{id}

多对一删除


首先,删除学生的话不会有任何影响,但是删除老师就意味着这个老师教授的学生的老师字段将不存在,所以需要将这些字段的值一并删除,才符合现实逻辑。

1
2
3
4
@Test
public void testDelete() {
teaDao.delete(2l);
}
  1. 要先将这个老师教授的学生与这个老师的关系删除,
  2. 再删除这个老师

StudentMapper.xml

1
2
3
4
5
6
7
<update id="deleteRelation">
update student_all2one set tea_id=null where tea_id=#{id}
</update>

<delete id="delete">
delete from student_all2one where id=#{id}
</delete>

TeacherMapper.xml

1
2
3
<delete id="delete">
delete from teacher_all2one where id=#{id}
</delete>

TeacherDAOImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void delete(Long id) {
SqlSession session = MybatisUtil.getConnection();
TeacherMapper teaMapper = session.getMapper(TeacherMapper.class);
StudentMapper stuMapper = session.getMapper(StudentMapper.class);
// 先删除关系 即执行如下sql,将所有学生和这个老师的关系斩断
// sql:update student_all2one set tea_id=null where tea_id=#{id}
stuMapper.deleteRelation(id);
teaMapper.delete(id);
session.commit();
session.close();
}

一对多(一个老师教多个学生)


由 Teacher类来维护 Teacher和 Student类之间的关系

表结构不会改变,但 domain类中的维护关系方变成 Teacher 了。

1
2
3
4
5
6
7
8
9
10
11
12
public class Student {
private Long id;
private String name;
private String password;
}

public class Teacher {
private Long id;
private String name;
private String sex;
private List<Student> stus = new ArrayList<Student>();
}

一对多(add)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private IStudentDAO stuDao = new StudentDAOImpl();
private ITeacherDAO teaDao = new TeacherDAOImpl();
@Test
public void testSave() {
// Teacher 类中包含 stus Student类的集合,所以先创建 Student 类
Student stu1 = new Student();
stu1.setName("小A");
stu1.setPassword("123456");
Student stu2 = new Student(null, "小B", "123456");
stuDao.save(stu1, stu2);

Teacher tea = new Teacher();
tea.setName("A");
tea.setSex("女");
tea.getStus().add(stu1);
tea.getStus().add(stu2);
teaDao.save(tea);

for (Student stu : tea.getStus()) {
// 添加依赖,维护 student表的 tea_id字段
stuDao.addRelation(tea.getId(), stu.getId());
}
}

分析:先插入多个 Student对象,然后创建 Teacher对象,最后添加依赖。由于维护关系的责任在 Teacher身上,所以 Student类不能在被添加到数据库时知道自己是哪个 Teacher的学生。只能等 Teacher分配好以后才能知道,所以最后需要维护 student表的 tea_id字段。

TeacherMapper.xml

1
2
3
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into teacher_one2all (name, sex) values(#{name}, #{sex})
</insert>

StudentMapper.xml

1
2
3
4
5
6
7
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into student_one2all (name, password) values(#{name}, #{password})
</insert>

<update id="addRelation">
update student_one2all set tea_id=#{teaId} where id=#{stuId}
</update>

StudentMapper.java

1
2
3
4
5
6
7
8
9
10
11
12
public interface StudentMapper {
void save(Student stu);
/**
* 多对一的情况,teacher 无法感知依赖,所以需要在保存的时候手动添加依赖
* @param stuId
* @param teaId
*/
void addRelation(@Param("teaId")Long teaId, @Param("stuId")Long stuId);

void delete(Long id);
void deleteRelation(Long id);
}

一对多select(额外 sql)


test,需要做到在数据库中查询出 teacher时,就能得到对应的学生列表

1
2
3
4
5
@Test
public void testGet() {
Teacher teacher = teaDao.get(39l); // 第一步
System.out.println(teacher);
}

由 teacher的 id属性到 student表中查询出所有 tea_id=id的学生,需要在 StudentMapper.xml 添加一个额外的方法用于查询出 Student集合。

TeacherMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<resultMap type="Teacher" id="teacherMap">
<id column="id" property="id"/>
<!-- colmn 把 teacher 表中的那一列作为参数,传到 select 里面去 -->
<!-- 第三步 -->
<collection property="stus" column="id"
select="cn.lizhaoloveit.mybatis.mapper.StudentMapper.getByTeaId">
</collection>
</resultMap>

<!-- 第二步 -->
<select id="getById" resultMap="teacherMap">
select * from teacher_one2all where id=#{id}
</select>

StudentMapper.xml

1
2
3
<select id="getByTeaId" resultType="Student">
select * from student_one2all where tea_id=#{teaId}
</select>

一对多(关联查询)


改动 DepartmentMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<resultMap type="Department" id="singleMap2">
<id column="id" property="id"/>
<result column="name" property="name"/>
<!-- 关联查询,一对多,实际上就是类中包含了一个集合变量
ofType 集合中每个元素的类型
-->
<collection property="es" ofType="Employee">
<!-- Employee 中的属性分别对应列中的那些字段 -->
<id column="e_id" property="id"/>
<result column="e_name" property="name"/>
</collection>
</resultMap>
<select id="get" resultMap="map">
select d.id, d.name, e.id e_id, e.name e_name
from department d left join employee e on d.id=e.dept_id
where d.id=#{id}
</select>

一对多(delete)


  1. 将 student表中的对应要删除的 tea_id 置空
  2. 删除 tea_id 的 teacher表中的数据。
1
2
3
4
@Test
public void testDelete() {
teaDao.delete(42l);
}

TeacherDAOImpl.java

1
2
3
4
5
6
7
8
9
10
11
@Override
public void delete(Long id) {
SqlSession session = MybatisUtil.getConnection();
TeacherMapper teaMapper = session.getMapper(TeacherMapper.class);
StudentMapper stuMapper = session.getMapper(StudentMapper.class);
// 先要解除关系,置空 tea_id 字段
stuMapper.deleteRelation(id);
teaMapper.delete(id);
session.commit();
session.close();
}

StudentMapper.xml

1
2
3
<update id="deleteRelation">
update student_one2all set tea_id=null where tea_id=#{teaId}
</update>

TeacherMapper.xml

1
2
3
<delete id="delete">
delete from teacher_one2all where id=#{id}
</delete>

多对多映射


表结构:

学生表:

2019-07-25 at 4.06 P

老师表:

2019-07-25 at 4.07 P

关系表:

2019-07-25 at 4.08 P

多对多(save)


1
2
3
4
5
6
7
8
9
10
11
12
13
public class Student {
private Long id;
private String name;
private String password;
private List<Teacher> teachers = new ArrayList<Teacher>();
}

public class Teacher {
private Long id;
private String name;
private String sex;
private List<Student> stus = new ArrayList<Student>();
}

test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Student s1 = new Student();
s1.setName("乔峰");
s1.setPassword("123456");
Student s2 = new Student();
s2.setName("张无忌");
s2.setPassword("123456");
Teacher t1 = new Teacher();
t1.setName("dafei");
t1.setSex("女");
Teacher t2 = new Teacher();
t2.setName("will");
t2.setSex("女");

// 关系维护
s1.getTeachers().add(t1);
s1.getTeachers().add(t2);
s2.getTeachers().add(t1);
s2.getTeachers().add(t2);

t1.getStus().add(s1);
t1.getStus().add(s2);
t2.getStus().add(s1);
t2.getStus().add(s2);

创建 Student 和 Teacher对象,然后维护对象之间的关系,然后就要保存对象到数据库,

1
2
3
4
5
6
7
8
9
10
11
12
stuDao.save(s1, s2); // 1
teaDao.save(t1, t2); // 2

// 维护中间表 student_teacher 保存这所有学生和老师的关系
for (Teacher t : s1.getTeachers()) {
stuDao.addRelation(t.getId(), s1.getId());
}

// 维护中间表
for (Teacher t : s2.getTeachers()) {
stuDao.addRelation(t.getId(), s2.getId());
}

操作 1和 2只是让数据进入到了表里,并没有对其关系做关联。因此需要额外表 student_teacher,保存 student 和 teacher表的关系。

StudentMapper.xml

1
2
3
4
5
6
7
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into student_all2all (name, password) values(#{name}, #{password})
</insert>

<insert id="addRelation">
insert into student_teacher (stu_id, tea_id) values(#{stuId}, #{teaId})
</insert>

TeacherMapper.xml

1
2
3
4
5
6
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into teacher_all2all (name, sex) values(#{name}, #{sex})
</insert>
<insert id="addRelation">
insert into student_teacher (stu_id, tea_id) values(#{stuId}, #{teaId})
</insert>

多对多(get 额外sql)


1
2
3
4
5
@Test
public void testGet() {
System.out.println(stuDao.get(9l));
System.out.println(teaDao.get(10l));
}

查询时,希望得到的 Student对象中 teachers可以被直接赋值。此时需要通过 resultMap

StudentMapper.xml

1
2
3
4
5
6
7
8
9
10
<resultMap type="Student" id="studentMap">
<id column="id" property="id"/>
<collection property="teachers" column="id"
select="cn.lizhaoloveit.mybatis.mapper.TeacherMapper.getByStuId">
</collection>
</resultMap>

<select id="getById" resultMap="studentMap">
select * from student_all2all where id=#{stuId}
</select>

Teacher.xml

1
2
3
4
5
6
<!-- 将老师表和关系表以老师id 键连接,查询出所有教某个学生的 老师集合 -->
<select id="getByStuId" resultType="Teacher">
select *
from teacher_all2all t left join student_teacher st on t.id=st.tea_id
where stu_id=#{stuId}
</select>

查询 Student,需要在关联表中通过 stu_id,查出所有与之匹配的 tea_id,就可以找到该学生被哪些老师教授了。

同理,想要得到 Teacher对象。需要根据 tea_id,在关联表中找出所有匹配的 stu_id

TeacherMapper.xml

1
2
3
4
5
6
7
8
9
10
<resultMap type="Teacher" id="teacherMap">
<id column="id" property="id"/>
<collection property="stus" column="id"
select="cn.lizhaoloveit.mybatis.mapper.StudentMapper.getByTeaId">
</collection>
</resultMap>

<select id="getById" resultMap="teacherMap">
select * from teacher_all2all where id=#{id}
</select>

StudentMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<select id="getByTeaId" resultType="Student">
select *
from student_all2all s left join student_teacher st on s.id = st.stu_id
where tea_id=#{teaId}
</select>

<!-- 也可以如下写法 -->
<select id="getByTeaId" resultType="Student">
select *
from student_all2all
where id in
(select stu_id from student_teacher where tea_id=#{teaId})
</select>

多对多(get 关联查询)


1
2
3
SELECT s.id, s.name, t.id t_id, t.name t_name FROM student s 
LEFT JOIN student_teacher st ON s.id = st.stu_id
LEFT JOIN teacher ON t.id = st.tea_id WHERE s.id = #{id}
1
2
3
4
5
6
7
8
<resultMap type="Student" id="StudentMap">
<id column="id" property="id"/>
<result column="name" property="name"/>
<collection property="ts" ofType="Teacher">
<id column="t_id" property="id"/>
<result column="t_name" property="name"/>
</collection>
</resultMap>
1
2
3
4
5
6
7
SqlSession session = MyBatisUtil.getSqlSession();
StudentMapper studentMapper = session.getMapper(StudentMapper.class);

Student student = studentMapper.get(1L);

session.commit();
session.close();

多对多(delete)


在删除其中一个表的一个数据时,需要先删除其与其他表的关联。即先删除中间表中存在该字段的行。再删除该数据

以删除 student 表中一个数据为例,(teacher 同理)

1
2
3
4
5
@Test
public void testDelete() {
teaDao.delete(9l);
stuDao.delete(10l);
}

StudentDAOImpl.java

1
2
3
4
5
6
7
8
9
@Override
public void delete(Long stuId) {
SqlSession session = MybatisUtil.getConnection();
StudentMapper mapper = session.getMapper(StudentMapper.class);
mapper.deleteRelation(stuId);
mapper.delete(stuId);
session.commit();
session.close();
}

StudentMapper.xml

1
2
3
4
5
6
7
<delete id="delete">
delete from student_all2all where id=#{stuId}
</delete>

<update id="deleteRelation">
delete from student_teacher where stu_id=#{stuId}
</update>

延迟加载问题


  • 什么是延迟加载

查询订单并且关联查询用户信息时,如果先查询订单信息即可满足要求,当我们需要查询用户信息时再查询用户信息,把对用户信息的按需查询叫做延迟加载,

延迟加载就是,先从单表查询,需要时再从关联表取查询,单标查询要比关联表查询速度快。

使用 association、collection 实现一对一及一对多映射,association 和 collection 还具备延迟加载功能,查询订单并且关联查询用户,查询用户使用延迟加载。

延迟加载要查询两次,第二次是按需查询,之前一对一关联查询的时候,只需要查一次,把订单和用户信息都查出来了,所以只要一个 mapper。但是延迟加载查两次,所以要有两个 mapper。

1
2
3
4
5
6
7
8
<!-- 只查询表单 -->
<select id="findOrdersUserLazyLoading" resultMap="OrdersUserLazyLoadingResultMap">
SELECT * FROM orders
</select>
<!-- 只查询用户 -->
<select id="findUserById" parameterType="int" resultType="user">
select * from user where id = #{id}
</select>

延迟加载的配置:
mybatis 默认没有开启延迟加载,需要在 SqlMapConfig.xml 中的一些配置,有一个<settings>,可以通过这个标签来配置一下延迟加载。

1
2
3
4
5
6
7
8
<settings>
<!-- 打开延迟加载的开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 将积极加载改为消极加载,即延迟加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 屏蔽掉 toString 方法 equals,clone,hashCode -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode"/>
</settings>
1
2
3
4
5
6
7
public interface UserMapperOrders {

//省去不相关代码

//查询订单,关联用户查询,用户查询用的是延迟加载
public List<Orders> findOrdersUserLazyLoading() throws Exception;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testFindOrdersUserLazyLoading() throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapperOrders userMapperOrders = sqlSession.getMapper(UserMapperOrders.class);
// 查询订单表(单表)
List<Orders> list = userMapperOrders.findOrdersUserLazyLoading();

// 遍历上边的订单列表
for(Orders orders : list) {
// 执行getUser()去查询用户信息,这里实现按需加载
User user = orders.getUser();
System.out.println(user);
}
}

使用了延迟加载,将关联查询分成了两次单表查询,第二次查询用户的时候,没有发送 sql 就拿到了数据,其实这是 mybatis 中的一级缓存。

延迟加载原理


深入一些可以发现 orders.getUser() 方法中并没有相关的 sql 执行,为什么在加载完 list 订单表时,没有执行 sql ,而在 orders.getUser() 调用时,发送了查询 user 表的 sql。这是为什么?

其实,Mybatis 使用了动态代理技术,在 Mybatis 内部会生成一个动态代理类 Orders$xxxx类,该类继承 Orders,并会拦截所有 public 方法。

当调用 Orders 的 getUser(),调用的是代理类的同名方法,代理类会引用一个实现了 InvocationHandler 接口的切面增强类,会调接口的 invoke 方法,将方法名和方法参数传入 invoke 中,然后增强类引用 Orders 真实类型,并调用 Orders 的方法并传入参数。在 Orders 方法调用前后实现增强,详情见动态代理原理

Orders$xxxx类,会有一个参数判断 User 是否已经加载,boolean userLoaded = false,在调用 getUser() 时,

1
2
3
4
5
6
7
8
9
public User getUser() {
if (!userLoaded) {
// 执行 UserMapper 的 getUser 方法
// 赋值
userLoaded = true;
// 返回
}
// 直接返回
}

Mybatis 缓存


缓存本质就是 Map,缓存是存在内存中的,可以在查询的时候减少数据库的访问次数,加快查询速度。

一级缓存


一级缓存的工作原理:

第一次发起查询用户id为1的用户信息,先去缓存中找是否有id为1的用户信息,如果没有,从数据库查询用户信息,得到用户信息,将用户信息存储到一级缓存中。

如果中间 sqlSession 去执行 commit 操作(插入、更新、删除),则会清空 SqlSession 中的一级缓存,这样做的目的是为了让缓存中存储的是最新的版本,避免脏读。

第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void testCache1() throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();//创建代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

//下边查询使用一个SqlSession
//第一次发起请求,查询id为1的用户
User user1 = userMapper.findUserById(1);
System.out.println(user1);

// 如果sqlSession去执行commit操作(执行插入、更新、删除),清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。

//更新user1的信息
user1.setUsername("测试用户22");
userMapper.updateUser(user1);
//执行commit操作去清空缓存
sqlSession.commit();

//第二次发起请求,查询id为1的用户
User user2 = userMapper.findUserById(1);
System.out.println(user2);
sqlSession.close();
}

一级缓存生命周期==session 的生命周期,在多个 session 中是无法尽心该数据共享。

在一次会话中,如果需要查询多次相同 id 的对象,此时后面几次的查询都会从缓存中获取,减少了访问数据库的次数,一级缓存的作用有限,只提高了一点点性能。

二级缓存


  1. 二级缓存生命周期== sessionFactory 的生命周期
  2. 二级缓存可以在不同的 session 之间进行数据的共享
  3. 二级缓存默认是关闭的,需要手动的去开启 <cache/>
  4. 不是所有对象都适合放到二级缓存中,只有读远远大于写的对象才适合放到二级缓存中
  5. 只要对象发生 DML 操作,MyBatis 中的二级缓存都会给清除
  6. 手动开启二级缓存,对应的 mapper.xml 文件添加一行:
    • <cache/> 同时,需要缓存的对象实现序列化接口

当内存中的对象已经达到设置的存储最大值,超出的对象如果需要也缓存起来,支持把缓存对象序列化到硬盘中,要获取的时候再反序列化回来
domain

1
2
3
public class USer implements Serializable {

}

UserMapper.xml

1
2
3
<mapper namespace="User.Mapper">
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/> <!-- 开启二级缓存,存到哪里 -->
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void testGet() throws Exception {
SqlSession session = MyBatisUtil.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);

User user = mapper.get(1l);

session.commit();
session.close();

session = MyBatisUtil.openSession();
mapper = session.getMapper(UserMapper.class);

user = mapper.get(1l);
session.commit();
session.close();
}

二级缓存的高效使用


需求: 开启二级缓存后,get(1l); insert(4l); get(1l) —> 总共发送3条 SQL

  1. 查询所有记录的查询语句,如果每次都先去二级缓存中获取,经常获取不到,然后又要去数据库中查询,效率低,查询所有不使用二级缓存:
    • useCache="false"
  2. insert 不会对 get(1L) 造成影响,所以 insert 只有在查询所有的时候才会有影响,出现脏读,可以让 insert 不刷新缓存

get(1L); insert(4l); get(1L) 就会只发送2条 SQL。第二次 get(1L) 是从二级缓存中获取的。

EhCashe 二级缓存


  • mybatis-ehcache-1.0.2.jar
  • ehcache-core-2.6.5.jar

导包

mybatis和ehcache包装的jar

1
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.1.0</version>
</dependency>

配置文件

名称固定 ehcache.xml

maxElementsInMemory: 该缓存池在内存中最大的缓存对象个数
eternal: 是否永久有效,如果设置为 true, 内存中对象永不过期
timeToIdleSeconds: 缓存对象最大空闲时间,单位: 秒;
timeToLiveSeconds: 缓存对象最大生存时间,单位: 秒;
overflowToDisk: 当内存中对象超过最大值,是否临时保存到磁盘
maxElementsOnDisk: 能保存到磁盘上最大对象数量
diskExpiryThreadIntervalSeconds: 磁盘失效线程运行时间间隔,默认是120秒
memoryStoreEvictionPolicy: 当达到 maxElementsInMemory 限制时, Ehcache 将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用),可以设置为 FIFO(先进先出)或者 LFU(较少使用)

如果我们Mapper想单独拥有一些特性,需要在mapper.xml中单独配置

1
2
3
4
5
6
7
8
9
10
<!-- 单位:毫秒 -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache">
<property name="timeToIdleSeconds" value="12000"/>
<property name="timeToLiveSeconds" value="3600"/>
<!-- 同ehcache参数maxElementsInMemory -->
<property name="maxEntriesLocalHeap" value="1000"/>
<!-- 同ehcache参数maxElementsOnDisk -->
<property name="maxEntriesLocalDisk" value="10000000"/>
<property name="memoryStoreEvictionPolicy" value="LRU"/>
</cache>

mybatis局限性


mybatis二级缓存对细粒度的数据级别的缓存实现不好,比如如下需求:

对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次都能查询最新的商品信息,此时如果使用mybatis的二级缓存就无法实现当一个商品变化时只刷新该商品的缓存信息而不刷新其它商品的信息,因为mybaits的二级缓存区域以mapper为单位划分,当一个商品信息变化会将所有商品信息的缓存数据全部清空。解决此类问题需要在业务层根据需求对数据有针对性缓存。

启动框架


目的:拿到数据库连接对象,操作增删改查

步骤:

  1. 加载主配置文件
  2. 获取 SqlSesstionFactory 对象(相当于连接池)
  3. 通过 factory 获取 SqlSesstion

插入


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void testInsert() throws Exception {
/**
* 1.加载主配置文件 ? 和 Thread.currentThread().
* getContextClassLoader()加载的区别
*/
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 获取 factory 对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
// 3. 获取 session 对象
SqlSession session = factory.openSession();
// 参数1:找到指定的 SQL,namespace + . + id
// 参数2:执行 SQL 是否需要给占位符设置参数
User u = new User(null, "谢谢", new BigDecimal("1000"), new Date());
System.out.println(u);
session.insert("cn.lizhaoloveit.mybatis.UserMapper.insert", u);
// 4.执行增删改,需要提交事务。
session.commit();
// 5.不要忘记关闭资源
session.close();
System.out.println(u);
}

Thread.currentThread().getContextClassLoader() 和 Class.getClassLoader() 的区别

前者是比较安全的用法,如果要在代码中动态加载 jar、资源文件的时候,首先应该使用 Thread.currentThread().getContextClassLoader(),因为如果你是用class.getClassLoader(),可能会导致和当前线程所运行的类加载器不一致,Java是天然的多线程,Java 只要类加载器不一致,加载出的类即使源文件一样,也会有不同的字节码对象。

Class.getClassLoader() 一般用在 getResource,因为你想获取某个资源文件的时候,这个资源文件的位置是相对固定的。

优化,1. 配置文件只需加载一次,整个应用只需要一个 SessionFactory 对象,也就是连接池对象

优化后代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static SqlSessionFactory factory = null;
private final static String USERMAPPER_PATH = "cn.lizhaoloveit.mybatis.UserMapper";
static {
try {
factory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (Exception e) {
e.printStackTrace();
}
}

@Test
public void testInsert() throws Exception {
// 获取 sesstion 对象
SqlSession session = factory.openSession();
//
User u = new User(null, "路驾驭", new BigDecimal("100"), new Date());
session.insert(USERMAPPER_PATH + ".insert", u);
// 提交
session.commit();
session.close();
}

修改


1
2
3
4
5
6
7
public void testUpdate() throws Exception {
SqlSession sesstion = factory.openSession();
User u = new User(9l, "路驾驭", new BigDecimal("100"), new Date());
sesstion.update(USERMAPPER_PATH + ".update", u);
sesstion.commit();
sesstion.close();
}

删除


1
2
3
4
5
6
public void testDelete() throws Exception {
SqlSession session = factory.openSession();
session.delete(USERMAPPER_PATH + ".delete", 3l);
session.commit();
session.close();
}

查询


1
2
3
4
5
6
public void testGet() throws Exception {
SqlSession session = factory.openSession();
User u = session.selectOne(USERMAPPER_PATH + ".get", 2l);
session.close();
System.out.println(u);
}
1
2
3
4
5
public void testGets() throws Exception {
SqlSession session = factory.openSession();
List<User> list = session.selectList(USERMAPPER_PATH + ".gets");
session.close();
}

MyBatis 操作流程


  1. 通过主配置文件来启动框架,目的主要是获取 SqlSession 对象
    告知框架四要素和事务处理,以及关联mapper映射文件。
  2. 使用 session 对象执行 SQL 命令。
    • 找到指定的 SQL 命令:namespace(cn.lizhaoloveit.mybatis.UserMapper.insert) + . + id
    • 传递执行该 SQL 需要的参数
  3. 如果执行 DML 操作,不要忘记提交事务。
  4. 关闭源

OGNL 表达式


#{} :mybatis 会将该语法进行翻译,将#{}直接使用 ? 来替换,相当于是一个 SQL 模板,创建一个预编译语句对象,给占位符设置值

#{} 就是一个 OGNL 表达式

OGNL (Object-Graph Navigation Language) 对象图形导航语言,可以存取对象的属性和调用对象的方法,遍历整个对象的结构图。

如何取值


#{} 可以直接取某个位置的属性
如果该位置是一个 javabean 对象,**#{属性名}**
如果该位置是一个 map 对象 #{key}
如果该位置是简单类型 #{任意}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Employee {
String name;
int age;
Dept dept;
}

class Dept {
String name;
}

/**
* #(name) 取出来该对象的名字
* #(age) 取出该对象的年龄
* #(dept) 取出对象的部门信息
* 表达式#{dept.name},表示访问了 employee 对象的 dept 属性的 name 属性
* 等价于 employee.getDept().getName()
* 只要知道放在箱子中的数据类型是什么,就可以通过 #{}方法获取数据,以后
* 应用最多的场景是往箱子中存 javabean
*
*/

什么时候往 session 中放的数据
使用 session 对象调用增删改查的时候,传递的参数就是放数据

什么时候取?
在 mapper 文件中取

使用日志


日志框架可以把日志的输出和代码分离,方便的定义日志的输出环境,控制台,文件,数据库,方便的定义日志的输出格式和输出级别。

ERROR > WARN > INFO > TRACE

导入日志框架包:

log4j-1.2.17.jar slf4j-api-1.7.25.jar slf4j-log4j12-1.7.25.jar—> stf4 规范

配置日志文件监控 MyBatis 的运行

log4j.properties; 日志文件在 classpath

1
2
3
4
5
6
7
8
# Global logging configuration
log4j.rootLogger=ERROR, stdout
# MyBatis logging configuration...
log4j.logger.cn.lizhaoloveit=TRACE
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

动态 SQL


如果你有 JDBC 或者其他相似框架的使用经验,就明白条件地串联 SQL 字符串在一起有多痛苦,确保不能忘了空格或者在列表的最后省略逗号。动态 SQL 可以彻底处理这种痛苦

通常使用动态 SQL 不可能是独立的一部分,MyBatis 使用一种强大的 动态 SQL 语言可以被用在任意映射的 SQL 语句中。

  • if
  • choose(when, otherwise)
  • trim(where, set)
  • (foreach)

xml 转义字符


字符实体 字符 意义
&lt; < 小于
&gt; > 大于
&amp; &
&apos; 单引号
&quot; 双引号

示例


查询类如下:

1
2
3
4
5
6
7
public class QueryStudent {
private String name;
private int minAge;
private int maxAge;
private QueryByPage qbp;
private int[] ages;
}

mapper.xml 内要做模糊匹配和范围查询,三种方式和第一个 select 标签内要查询同一个内容

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
35
36
37
38
39
<select id="queryForName" resultType="Student">
select * from student
where name like concat('%', concat(#{name}, '%'))
and age &lt; #{maxAge} and age &gt; #{minAge}
limit #{qbp.beginIndex}, #{qbp.pageSize}
</select>

<select id="queryForName" resultType="Student">
select * from student
<where>
<if test="name != null">
name like concat('%', concat(#{name}, '%'))
</if>
<if test="maxAge != null">
and age &lt; #{maxAge}
</if>
<if test="minAge != null">
and age &gt; #{minAge}
</if>
</where>
limit #{qbp.beginIndex}, #{qbp.pageSize}
</select>

<select id="queryForName" resultType="Student">
select * from student
<where>
<if test="name != null">
name like concat('%', concat(#{name}, '%'))
</if>
<if test="ages != null">
and age in
<foreach collection="ages" item="age" index="index"
open="(" separator="," close=")">
#{age}
</foreach>
</if>
</where>
limit #{qbp.beginIndex}, #{qbp.pageSize}
</select>

if


1
2
3
4
5
6
7
8
<select id="findActiveBlogWithTitleLike" 
parameterType="Blog" resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null"> --如果title != null if 标签--
AND title like #{title}
</if>
</select>

如果没有 title state=’ACTIVE’ 的 blog 将会被返回,如果传递了 title,就会查找相近的 title 的 blog。

如果我们想可选的搜索 title 和 auther ,加入另一个简单的条件即可

1
2
3
4
5
6
7
8
9
10
<select id="findActiveBlogLike" 
parameterType="Blog" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>

choose, when, otherwise


有时候,不想应用所有的条件,很多情况下只选择一种,和 Java 中的 switch 语句相似,MyBatis 提供 choose 元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="findActiveBlogLike" 
parameterType="Blog" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>

trim, where, set


如果 Where 后面的语句都是条件语句。就可以会出现问题,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="findActiveBlogLike" 
parameterType="Blog" resultType="Blog">
SELECT * FROM BLOG
WHERE
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>

如果这些条件都没有匹配上,这条 SQL 结束时就会成这样:

1
2
SELECT * FROM BLOG 
WHERE

如果仅仅第二条语句匹配:

1
2
3
SELECT * FROM BLOG 
WHERE
AND title like ‘someTitle’

where 标签就是用来解决这个问题,如果以 “AND” “OR” 开头的内容,就会跳过不插入 “AND” 或者 “OR”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="findActiveBlogLike" 
parameterType="Blog" resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>

如果 where 标签没有做出你想要的,可以使用 trim 标签自定义,比如:

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

和 where 类似,set 在更新时,也可能没有要更新的列字段。此时就会变成

1
update Author set where id = ...
1
2
3
4
5
6
7
8
9
10
11
<update id="updateAuthorIfNecessary"
parameterType="domain.blog.Author">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>

foreach


如果你需要动态在范围查找,构建一个集合,你可以传递一个 List 对象或者数组作为参数,MyBatis 会自动将它包装在一个 Map 中,名称作为键,List对象会以 “list” 作为键,数组会以 “array” 作为键。

1
2
3
4
5
6
7
8
9
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>

如果User有属性List ids。入参是User对象,那么这个collection = “ids”
如果User有属性Ids ids;其中 Ids 是个对象,Ids 有个属性 List id;
入参是User对象,那么collection = “ids.id”
如果参数是一个数组、或者一个集合,应该用 array 或者 list 来获取

批量插入和批量删除


bind

效果:可以把一段字符串拼接成一个属性”name”值 中

1
2
3
4
5
<select id="selectBlogsLike" parameterType="Blog" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>

Multi-db vendor support

支持不同的数据库一起查询

1
2
3
4
5
6
7
8
9
10
11
<insert id="insert" parameterType="org.myapp.domain.User">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
<if test="_databaseId == 'oracle'">
select seq_users.nextval from dual
</if>
<if test="_databaseId == 'db2'">
select nextval for seq_users from sysibm.sysdummy1"
</if>
</selectKey>
insert into users values (#{id}, #{name})
</insert>

MyBatis 3.2 开始,支持可插拔的脚本语言。

1
2
3
<select id="selectBlog" lang="raw">
SELECT * FROM BLOG
</select>
1
2
3
4
5
6
7
8
9
10
11
public interface Mapper {
@Lang(RawLanguageDriver.class)
@Select("SELECT * FROM BLOG")
List<Blog> selectBlog();
}

public interface LanguageDriver {
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}
文章作者: Ammar
文章链接: http://lizhaoloveit.cn/2019/07/06/MyBatis/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ammar's Blog
打赏
  • 微信
  • 支付宝

评论