ORM(Object Relational Mapping)
ORM思想:为了解决面向对象与关系数据库存在的互不匹配的现象的技术。ORM是通过使用描述对象和数据库之间映射的元数据,将java程序中的对象自动持久化到关系数据库中。将关系数据库中表中的记录映射成对象,以对象的形式展现。把对数据库的操作转化为对对象的操作。因此 ORM 的目的是为了让开发人员以面向对象的思想来实现对数据库的操作。
目前流行的 ORM 框架:
JPA : 一种 ORM 规范,需要各大 ORM 框架实现,JavaEE 的规范,Java persistence api : Java 持久化 API
Hibernate : 最流行的 ORM 框架,设计灵巧,性能优秀,文档丰富
MyBatis : 本是 apache 的一个开源项目 iBatis,提供的持久层框架包括SQL、Maps和DAO,允许开发人员直接编写SQL
mybatis 概述
MyBatis 是一个 SQL 的映射框架,其前身是 iBatis,也就是淘宝试用的持久层框架,org.apache.ibatis
mybatis 主要功能
几乎消除了所有 JDBC 代码和参数的手工设置以及结果集都可以交给 MyBatis完成
这些只需要简单的使用 XML 或者 注释配置即可
使用步骤
使用配置文件(主配置文件/总配置文件/全局配置文件),告知框架连接数据库四要素
使用配置文件(SQL映射文件),告知框架执行哪些SQL
启动框架,让框架执行具体SQL任务
框架从对应的配置文件中获取数据(DOM 解析),其他的事情都已经做好了。
准备工作
导入 mybatis
导入 mybatis 核心包和 mysql 驱动包
创建数据库表
创建domain类
MyBatis-config.XML
又叫 主配置文件/总配置文件/全局配置文件
主要是告知框架 连接数据库四要素,事务处理,关联mapper文件,启动框架时,只会加载主配置文件
规范
起名:mybatis-config.xml
路径,放在classpath 下面,创建一个 source folder:resources
参考文档: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 > <properties resource ="db.properties" > </properties > <typeAliases > <package name ="cn.lizhaoloveit.mybatis" /> </typeAliases > <environments default ="dev" > <environment id ="dev" > <transactionManager type ="JDBC" > </transactionManager > <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 > <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语句 )
规范
起名:xxxMapper.xml -> xxx 表示 domain, userMapper.xml
路径:映射文件跟 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 <mapper namespace ="cn.wolfcode.UserMapper" > <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 <select id ="listAll" resultMap ="basemap" > </select > <resultMap type ="cn.lizhaoloveit.User" id ="basemap" > <id / cloumn ="uid" property ="id" /> <result column ="uname" property ="name" /> <result column ="usalary" property ="salary" /> <result column ="uhiredate" property ="hiredate" /> </resultMap >
只要是查询,一定有结果类型。
使用 mapper 接口
创建一个 Mapper 接口,这个接口的要求:
这个接口的全限定名===对应的 Mapper 文件的 namespace
这个接口中的方法和 Mapper 文件的 SQL 元素一一对应:方法名字=SQL元素id,方法的参数类型=SQL元素中定义的 parameterType 类型,方法的返回类型=SQL元素中定义的resultType/resultMap 类型’
创建 sqlSession
通过 sqlSession.getMapper(xxxMapper.class) 方法得到一个 Mapper 对象
调用 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 ="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的关系。
项目结构如下:
表结构:
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 () { 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 ="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 select ="全限定名.get" property ="dept" column ="dept_id" /> </resultMap >
使用了继承,是因为订单信息和关联的用户信息和前面一对一是完全一样的,不需要再写一遍,resultMap 支持继承,直接继承那个 resultMap 即可,然后加上订单明细这部分。
N+1 sql (多对一查询)
问题:多对一 n+1 问题是指以下情况,假设系统中存在两张表,学生表(多),和老师表(1), 学生表字段:sid,sname,tid。老师表字段为 tid tname , 两张表通过 tid关联。
现在假设一个页面需要浏览学生记录,同时要显示这个学生对应的老师名称,有两种查询方法。
n+1 句 sql,先查询 select * from 学生表。再根据结果记录查询 select * from 老师表 where tid = ?
建立一条联合查询语句 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 ="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 =
多对一删除
首先,删除学生的话不会有任何影响,但是删除老师就意味着这个老师教授的学生的老师字段将不存在,所以需要将这些字段的值一并删除,才符合现实逻辑。
1 2 3 4 @Test public void testDelete () { teaDao.delete(2l ); }
要先将这个老师教授的学生与这个老师的关系删除,
再删除这个老师
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); 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 () { 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()) { 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) ; 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" /> <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" /> <collection property ="es" ofType ="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)
将 student表中的对应要删除的 tea_id 置空
删除 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); 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 >
多对多映射
表结构:
学生表:
老师表:
关系表:
多对多(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); teaDao.save(t1, t2); 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 <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_idLEFT JOIN teacher ON t.id = st.tea_id WHERE s.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" /> <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) { 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) { 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); User user1 = userMapper.findUserById(1 ); System.out.println(user1); user1.setUsername("测试用户22" ); userMapper.updateUser(user1); sqlSession.commit(); User user2 = userMapper.findUserById(1 ); System.out.println(user2); sqlSession.close(); }
一级缓存生命周期==session 的生命周期,在多个 session 中是无法尽心该数据共享。
在一次会话中,如果需要查询多次相同 id 的对象,此时后面几次的查询都会从缓存中获取,减少了访问数据库的次数,一级缓存的作用有限,只提高了一点点性能。
二级缓存
二级缓存生命周期== sessionFactory 的生命周期
二级缓存可以在不同的 session 之间进行数据的共享
二级缓存默认是关闭的,需要手动的去开启 <cache/>
不是所有对象都适合放到二级缓存中,只有读远远大于写的对象才适合放到二级缓存中
只要对象发生 DML 操作,MyBatis 中的二级缓存都会给清除
手动开启二级缓存,对应的 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
查询所有记录的查询语句,如果每次都先去二级缓存中获取,经常获取不到,然后又要去数据库中查询,效率低,查询所有不使用二级缓存:
useCache="false"
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" /> <property name ="maxEntriesLocalHeap" value ="1000" /> <property name ="maxEntriesLocalDisk" value ="10000000" /> <property name ="memoryStoreEvictionPolicy" value ="LRU" /> </cache >
mybatis局限性
mybatis二级缓存对细粒度的数据级别的缓存实现不好,比如如下需求:
对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次都能查询最新的商品信息,此时如果使用mybatis的二级缓存就无法实现当一个商品变化时只刷新该商品的缓存信息而不刷新其它商品的信息,因为mybaits的二级缓存区域以mapper为单位划分,当一个商品信息变化会将所有商品信息的缓存数据全部清空。解决此类问题需要在业务层根据需求对数据有针对性缓存。
启动框架
目的:拿到数据库连接对象,操作增删改查
步骤:
加载主配置文件
获取 SqlSesstionFactory 对象(相当于连接池)
通过 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 { InputStream in = Resources.getResourceAsStream("mybatis-config.xml" ); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in); SqlSession session = factory.openSession(); User u = new User(null , "谢谢" , new BigDecimal("1000" ), new Date()); System.out.println(u); session.insert("cn.lizhaoloveit.mybatis.UserMapper.insert" , u); session.commit(); 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 { 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 操作流程
通过主配置文件来启动框架,目的主要是获取 SqlSession 对象 告知框架四要素和事务处理,以及关联mapper映射文件。
使用 session 对象执行 SQL 命令。
找到指定的 SQL 命令:namespace(cn.lizhaoloveit.mybatis.UserMapper.insert) + . + id
传递执行该 SQL 需要的参数
如果执行 DML 操作,不要忘记提交事务。
关闭源
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; }
什么时候往 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 log4j.rootLogger =ERROR, stdout log4j.logger.cn.lizhaoloveit =TRACE 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 转义字符
字符实体
字符
意义
<
<
小于
>
>
大于
&
&
和
'
‘
单引号
"
“
双引号
示例
查询类如下:
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 < #{maxAge} and age > #{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 < #{maxAge} </if > <if test ="minAge != null" > and age > #{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) ; }