本系列文章:
Mybatis(一)Mybatis的基本使用与工作原理
Mybatis(二)Mybatis的高级使用
Mybatis(三)配置文件解析流程
Mybatis(四)映射文件解析流程
Mybatis(五)SQL执行流程
Mybatis(六)数据源、缓存机制、插件机制
要了解ORM,先了解下面概念:
ORM
,即Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间作一个映射
。这样在具体的操作业务对象的时候,就不需要再去和复杂的SQL语句打交道,只需简单的操作对象的属性和方法。
总结:
- 它是一种
将内存中的对象保存到关系型数据库中
的技术;- 主要
负责实体对象的持久化
,封装数据库访问细节;- 提供了实现持久化层的另一种模式,采用映射元数据(XML)来描述对象-关系的映射细节,使得ORM中间件能在任何一个Java应用的业务逻辑层和数据库之间充当桥梁。
Java典型的ORM框架:
1)hibernate:全自动的框架,强大、复杂、笨重、学习成本较高
;
2)Mybatis
:半自动的框架, 必须要自己写sql
;
3)JPA:JPA全称Java Persistence API、JPA通过JDK 5.0注解或XML描述对象-表的映射关系,是Java自带的框架。
MyBatis是一款持久层半ORM
框架,它支持定制化SQL、存储过程
以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和Java的POJO(Plain Old Java Objects,普通老式Java对象)为数据库中的记录。
MyBatis的核心配置文件是mybatis-config.xml。
储存过程是实现某个特定功能的一组sql语句集,是经过编译后存储在数据库中
。当出现大量的事务回滚或经常出现某条语句时,使用存储过程的效率往往比批量操作要高得多。 create table student
(
id bigint not null,
name varchar(30),
sex char(1),
primary key (id)
);
有一个添加记录的存储过程:
create procedure pro_addStudent (IN id bigint, IN name varchar(30), IN sex char(1))
begin
insert into student values (id, name, sex);
end
此时就可以在mapper.xml文件中调用存储过程:
<select id="findStudentById" parameterType="java.lang.Long" statementType="CALLABLE"
resultType="com.mybatis.entity.Student">
{call pro_getStudent(#{id,jdbcType=BIGINT,mode=IN})}
select>
<parameterMap type="java.util.Map" id="studentMap">
<parameter property="id" mode="IN" jdbcType="BIGINT"/>
parameterMap>
<select id="findStudentById" parameterMap="studentMap" statementType="CALLABLE"
resultType="com.mybatis.entity.Student">
{call pro_getStudent(?)}
select>
<mapper namespace="com.test.pojo">
<select id="listCategoryByName" parameterType="string" resultType="Category">
select * from category_ where name like concat('%',#{0},'%')
select>
mapper>
2)在Java代码中调用此语句,示例:
List<Category> cs = session.selectList("listCategoryByName","cat");
for (Category c : cs) {
System.out.println(c.getName());
}
<mapper namespace="com.test.pojo">
<select id="listCategory" resultType="Category">
select * from category_
</select>
</mapper>
MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。
MyBatis框架的适用场景:对性能的要求很高,或者需求变化较多的项目
,如互联网项目。
Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。
而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成
,所以,称之为半自动ORM映射工具。
MyBatis作为半自动ORM映射工具与全自动ORM工具相比,有几个主要的区别点:
MyBatis作为一种半自动ORM映射工具,相对于全自动ORM工具具有更高的灵活性和可定制性。通过灵活的SQL控制、自定义的映射关系、可复用的SQL以及灵活的性能调优,MyBatis可以满足各种复杂的映射需求和性能优化需求。虽然MyBatis相对于全自动ORM工具需要开发人员编写更多的SQL语句,但正是由于这种半自动的特性,使得MyBatis在某些复杂场景下更加灵活和可控。
因此,我们可以说MyBatis是一种半自动ORM映射工具,与全自动的ORM工具相比,它更适用于那些对SQL灵活性和性能调优需求较高的场景。
Mybatis有以下优点:
SQL写在XML里,解除sql与程序代码的耦合
,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码
,不需要手动开关连接。 --------------------------------------------------------------------------------------------------------
Mybatis有以下缺点:
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="SQL Server" value="sqlserver"/>
<property name="Oracle" value="oracle"/>
databaseIdProvider>
然后在xml文件中,就可以针对不同的数据库,写不同的sql语句。
public class Student{
String name;
List<Interest> interests;
}
public class Interest{
String studentId;
String name;
String direction;
}
<resultMap id="ResultMap" type="com.test.Student">
<result column="name" property="name" />
<collection property="interests" ofType="com.test.Interest">
<result column="name" property="name" />
<result column="direction" property="direction" />
collection>
resultMap>
在该例子中,如果查询sql中,没有关联Interest对应的表,则查询出数据映出的Student对象中,interests属性值就会为空。
- 执行了一个单独的SQL语句来获取结果的一个列表(就是“1”)。
- 对列表返回的每条记录,执行一个select查询语句来为每条记录加载详细信息(就是“N”)。
其实就是相关联的两步查询,可能会出现"N+1"的情况。修改方法如下:
1、在sql语句中使用join语句连接多张表并进行查询(常用)。
2、使用懒加载技术,延迟“N查询”部分中各操作被执行的时间节点;
3、合并N查询为一个查询,通过使用Sql的关键字in。
//错误写法
startPage();
List<User> list;
if(user != null){
list = userService.selectUserList(user);
} else {
list = new ArrayList<User>();
}
Post post = postService.selectPostById(1L);
return getDataTable(list);
//正确写法
List<User> list;
if(user != null){
startPage();
list = userService.selectUserList(user);
} else {
list = new ArrayList<User>();
}
Post post = postService.selectPostById(1L);
return getDataTable(list);
以上代码错误的原因是:由于user存在null的情况,就会导致pageHelper生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。 当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
写法2:
//错误写法
startPage();
Post post = postService.selectPostById(1L);
List<User> list = userService.selectUserList(user);
return getDataTable(list);
//正确写法
Post post = postService.selectPostById(1L);
startPage();
List<User> list = userService.selectUserList(user);
return getDataTable(list);
以上代码错误的原因是:只对开启分页后的第一个查询sql语句查到的数据进行分页
。
JDBC(Java Data Base Connection,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。
传统的JDBC开发,通常是如下的流程:
加载驱动;
建立连接;
定义sql语句;
准备静态处理块对象;
执行sql语句;
处理结果集;
关闭连接.
传统的JDBC开发的最原始的开发方式,有以下问题:
1、频繁创建数据库连接对象、释放,容易造成系统资源浪费,影响系统性能
。可以使用连接池解决这个问题,但是使用JDBC需要自己实现连接池。实际项目中sql语句变化的可能性较大,一旦发生变化,需要修改java代码,系统需要重新编译
,重新发布,不好维护。3、使用preparedStatement向占有位符号传参数存在硬编码
,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护。4、结果集处理存在重复代码,处理麻烦。如果可以映射成Java对象会比较方便
。 这点也容易理解,在使用JDBC时,需要用 ResultSet之类的方式来遍历数据库中查询出来的一条条字段,这是不方便的,示例: /*ResultSet:查询结果集*/
ResultSet rs = s.executeQuery(sql);
while (rs.next()) {
/*可以使用字段名获取该列内容*/
int id = rs.getInt("id");
/*也可以使用字段的顺序,需要注意的是:顺序是从1开始的*/
String name = rs.getString(2);
float hp = rs.getFloat("hp");
int damage = rs.getInt(4);
System.out.printf("%d\t%s\t%f\t%d%n", id, name, hp, damage);
}
在mybatis-config.xml中配置数据连接池,使用连接池管理数据库连接
。将Sql语句配置在XXXXmapper.xml文件中,与Java代码分离
。相同点:都是对JDBC的封装,都是持久层的框架,都用于dao层的开发。
不同点1:映射关系
MyBatis是一个半自动映射的框架,配置Java对象与sql语句执行结果的对应关系,多表关联关系配置简单。
Hibernate是一个全表映射的框架,配置Java对象与数据库表的对应关系,多表关联关系配置复杂。
不同点2:SQL优化和移植性
Hibernate对SQL语句封装,提供了日志、缓存、级联(级联比MyBatis强大)等特性, 此外还提供HQL(Hibernate Query Language)操作数据库,数据库无关性支持好,但会多消耗性能。如果项目需要支持多种数据库,代码开发量少,但SQL语句优化困难。
MyBatis需要手动编写SQL,支持动态SQL、处理列表、动态生成表名、支持存储过程。 开发工作量相对大些。直接使用SQL语句操作数据库,不支持数据库无关性,但sql语句优 化容易。
不同点3:开发难易程度和学习成本
Hibernate是重量级框架,学习使用门槛高,适合于需求相对稳定,中小型的项目,比如: 办公自动化系统。
MyBatis是轻量级框架,学习使用门槛低,适合于需求变化频繁,大型的项目,比如:互 联网电子商务系统。
Hibernate优势
1)Hibernate的DAO层开发比MyBatis简单
,Mybatis需要维护SQL和结果映射。
2)Hibernate对对象的维护和缓存要比MyBatis好
,对增删改查的对象的维护要方便。
3)Hibernate数据库移植性很好
,MyBatis的数据库移植性不好,不同的数据库需要写不同SQL。
4)Hibernate有更好的二级缓存机制,可以使用第三方缓存
。MyBatis本身提供的缓存机制不佳。
Mybatis优势
1)MyBatis可以进行更为细致的SQL优化,可以减少查询字段。
2)MyBatis容易掌握,而Hibernate门槛较高。
3)sql语句和Java代码耦合性低。
总结
MyBatis是一个小巧、方便、高效、简单、直接、半自动化的持久层框架,
Hibernate是一个强大、方便、高效、复杂、间接、全自动化的持久层框架。
以下两种方式了解即可,实际开发中不会这样使用Mybatis。
- 创建SqlSessionFactory;
- 通过SqlSessionFactory创建SqlSession;
- 通过sqlsession执行数据库操作;
- 调用session.commit()提交事务;
- 调用session.close()关闭会话。
示例:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
Category c = new Category();
c.setName("新增加的Category");
session.insert("addCategory",c);
session.commit();
session.close();
- 创建SqlSessionFactory对象。
- 通过SqlSessionFactory获取SqlSession对象。
- 通过SqlSession获得Mapper代理对象。
- 通过Mapper代理对象,执行数据库操作。
- 执行成功,则使用SqlSession提交事务。
- 执行失败,则使用SqlSession回滚事务。
- 关闭session会话。
示例:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
CategoryMapper mapper = session.getMapper(CategoryMapper.class);
List<Category> cs = mapper.list();
for (Category c : cs) {
System.out.println(c.getName());
}
session.commit();
session.close();
同理,以下两种方式了解即可。
先在数据库建一张表:表名为student,字段有id、name、score、age、gender。
package com.test.po;
import lombok.*;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Student {
private Integer id;
private String name;
private Integer score;
private Integer age;
private Integer gender;
}
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="test">
<select id="findAll" resultType="com.test.po.Student">
SELECT * FROM student;
select>
<insert id="insert" parameterType="com.test.po.Student">
INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender});
insert>
<delete id="delete" parameterType="int">
DELETE FROM student WHERE id = #{id};
delete>
mapper>
配置数据库连接信息:
db.url=jdbc:mysql://localhost:3306/yogurt?characterEncoding=utf8
db.user=root
db.password=root
db.driver=com.mysql.jdbc.Driver
DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="properties/db.properties">properties>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${db.driver}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.user}"/>
<property name="password" value="${db.password}"/>
dataSource>
environment>
environments>
<mappers>
<mapper resource="StudentMapper.xml"/>
mappers>
configuration>
此处调用XXXMapper.xml文件中的SQL语句。
package com.test.dao;
import com.test.po.Student;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class StudentDao {
private SqlSessionFactory sqlSessionFactory;
public StudentDao(String configPath) throws IOException {
InputStream inputStream = Resources.getResourceAsStream(configPath);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
public List<Student> findAll() {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Student> studentList = sqlSession.selectList("findAll");
sqlSession.close();
return studentList;
}
public int addStudent(Student student) {
SqlSession sqlSession = sqlSessionFactory.openSession();
int rowsAffected = sqlSession.insert("insert", student);
sqlSession.commit();
sqlSession.close();
return rowsAffected;
}
public int deleteStudent(int id) {
SqlSession sqlSession = sqlSessionFactory.openSession();
int rowsAffected = sqlSession.delete("delete",id);
sqlSession.commit();
sqlSession.close();
return rowsAffected;
}
}
此处用测试类代替:
public class SimpleTest {
private StudentDao studentDao;
@Before
public void init() throws IOException {
studentDao = new StudentDao("mybatis-config.xml");
}
@Test
public void insertTest() {
Student student = new Student();
student.setName("yogurt");
student.setAge(24);
student.setGender(1);
student.setScore(100);
studentDao.addStudent(student);
}
@Test
public void findAllTest() {
List<Student> all = studentDao.findAll();
all.forEach(System.out::println);
}
}
结果示例:
mybatis-config.xml是Mybatis使用的核心配置文件。在该文件中,各个标签是有顺序的,因为mybatis加载配置文件的源码中是按照这个顺序进行解析的:
<configuration>
configuration>
1、
${}
占位符快速获取数据源的信息。2、
来开启延迟加载,可以用
来开启二级缓存。3、
<typeAliases>
<typeAlias type="com.test.po.Student" alias="student"/>
typeAliases>
之后就可以在resultType上直接写student,mybatis会根据别名配置自动找到对应的类。当然,如果想要一次性给某个包下的所有类设置别名,可以用如下的方式:
<typeAliases>
<package name="com.test.po"/>
typeAliases>
如此,指定包下的所有类,都会以简单类名的小写形式,作为它的别名。
别名使用时是不区分大小写的
。
另外,对于基本的Java类型 -> 8大基本类型以及包装类,以及String类型,mybatis提供了默认的别名,别名为其简单类名的小写,比如原本需要写java.lang.String,其实可以简写为string。
Mybatis默认的别名在TypeAliasRegistry中进行注册的,这个类就是Mybatis注册别名使用的,别名和具体的类型关联是放在这个类的一个map属性(typeAliases)中。
Mybatis默认为很多类型提供的别名:
别名 | 对应的实际类型 |
---|---|
_byte | byte |
_long | long |
_short | short |
_int | int |
_integer | int |
_double | double |
_float | float |
_boolean | boolean |
string | String |
byte | Byte |
long | Long |
short | Short |
int | Integer |
integer | Integer |
double | Double |
float | Float |
boolean | Boolean |
date | Date |
decimal | BigDecimal |
bigdecimal | BigDecimal |
object | Object |
map | Map |
hashmap | HashMap |
list | List |
arraylist | ArrayList |
collection | Collection |
iterator | Iterator |
别名的原理:Mybatis允许我们给某种类型注册一个别名,别名和类型之间会建立映射关系,这个映射关系存储在一个map对象中,key为别名的名称,value为具体的类型。当我们通过一个名称访问某种类型的时候,Mybatis根据类型的名称,先在别名和类型映射的map中按照key进行查找,如果找到了直接返回对应的类型。如果没找到,会将这个名称当做完整的类名去解析成Class对象,如果这2步解析都无法识别这种类型,就会报错。
4、
5、
6、
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="helperDialect" value="mysql"/>
plugin>
plugins>
7、
8、
提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
MyBatis和数据库的交互有两种方式:使用传统的MyBatis提供的API、使用Mapper接口。
SqlSession session = sqlSessionFactory.openSession();
Category c = new Category();
c.setName("新增加的Category");
session.insert("addCategory",c);
上述使用MyBatis的方法,是创建一个和数据库打交道的SqlSession对象,然后根据Statement Id和参数来操作数据库,这种方式固然很简单和实用,但是它不符合面向对象语言的概念和面向接口编程的编程习惯。
节点抽象为一个Mapper接口,而这个接口中声明的方法和跟
节点中的 SqlSession session = sqlSessionFactory.openSession();
CategoryMapper mapper = session.getMapper(CategoryMapper.class);
List<Category> cs = mapper.list();
for (Category c : cs) {
System.out.println(c.getName());
}
根据MyBatis的配置规范配置后,通过SqlSession.getMapper(XXXMapper.class)
方法,MyBatis会根据相应的接口声明的方法信息,通过动态代理机制生成一个Mapper实例。使用Mapper接口的某一个方法时,MyBatis会根据这个方法的方法名和参数类型,确定Statement Id,底层还是通过SqlSession.select("statementId",parameterObject)
或者SqlSession.update("statementId",parameterObject)
等等来实现对数据库的操作。
MyBatis引用Mapper接口这种调用方式,纯粹是为了满足面向接口编程的需要。
负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
List
。MyBatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。 负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理
,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。
每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象
。重复使用Statement对象
。批处理
中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。
指定Executor方式有两种:
<settings>
<setting name="defaultExecutorType" value="BATCH" />
settings>
// 获取指定执行器的sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
// 获取批量执行器时, 需要手动提交事务
sqlSession.commit();
MyBatis中的延迟加载,也称为懒加载,是指在进行表的关联查询时,按照设置延迟规则推迟对关联对象的select查询。例如在进行一对多查询的时候,只查询出一方,当程序中需要多方的数据时,mybatis再发出sql语句进行查询,这样子延迟加载就可以的减少数据库压力。MyBatis 的延迟加载只是对关联对象的查询有迟延设置,对于主加载对象都是直接执行查询语句的。
假如Clazz 类中有子对象HeadTeacher。两者的关系:
public class Clazz {
private Set<HeadTeacher> headTeacher;
//...
}
是否查出关联对象的示例:
@Test
public void testClazz() {
ClazzDao clazzDao = sqlSession.getMapper(ClazzDao.class);
Clazz clazz = clazzDao.queryClazzById(1);
//只查出主对象
System.out.println(clazz.getClassName());
//需要查出关联对象
System.out.println(clazz.getHeadTeacher().size());
}
在Mybatis中,延迟加载可以分为两种:延迟加载属性和延迟加载集合,association关联对象
和collection关联集合对象
的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false
。
<settings>
<setting name="lazyLoadingEnabled" value="true" />
<setting name="aggressiveLazyLoading" value="false"/>
settings>
比如class班级与student学生之间是一对多关系。在加载时,可以先加载class数据,当需要使用到student数据时,我们再加载 student 的相关数据。
侵入式延迟加载,指的是只要主表的任一属性加载,就会触发延迟加载,比如:class的name被加载,student信息就会被触发加载。
深度延迟加载指的是只有关联的从表信息被加载,延迟加载才会被触发。通常,更倾向使用深度延迟加载。
<association
property="dept" select="com.test.dao.DeptDao.getDeptAndEmpsBySimple"
column="deptno" fetchType="eager"/>
fetchType值有2种,eager:立即加载;lazy:延迟加载。 由于局部的加载策略的优先级高于全局的加载策略。指定属性后,将在映射中忽略全局配置参数lazyLoadingEnabled,使用属性的值。
MyBatis使用Java动态代理来为查询对象生成一个代理对象。当访问代理对象的属性时,MyBatis会检查该属性是否需要进行延迟加载。如果需要延迟加载,则MyBatis将再次执行SQL查询,并将查询结果填充到代理对象中。
缓存分为一级缓存和二级缓存。一级缓存:线程级别的缓存,sqlSession级别的缓存;二级缓存:全局范围的缓存。默认情况下一级缓存是开启的,而且是不能关闭的。
Mybatis缓存机制示意图:
一级缓存是Session会话级别的,一般而言,一个SqlSession对象会使用一个Executor对象来完成会话操作,Executor对象会维护一个Cache缓存,以提高查询性能。
每当使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话。
一级缓存是指SqlSession级别的缓存,当在同一个SqlSession中进行相同的SQL语句查询时,第二次以后的查询不会从数据库查询,而是直接从缓存中获取。一级缓存最多缓存1024条SQL
。
在对数据库的一次会话中,我们有可能会反复地执行完全相同的查询语句,如果不采取一些措施的话,每一次查询都会查询一次数据库,而我们在极短的时间内做了完全相同的查询,那么它们的结果极有可能完全相同,由于查询一次数据库的代价很大,这有可能造成很大的资源浪费。为了解决这一问题,减少资源的浪费,MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。这就是一级缓存。
一级缓存默认开启,同一个SqlSesion级别共享的缓存,在一个SqlSession的生命周期内,执行2次相同的SQL查询,则第二次SQL查询会直接取缓存的数据,而不走数据库
。
当然,若第一次和第二次相同的SQL查询之间,执行DML(增删改),则一级缓存会被清空,第二次查询相同SQL仍然会走数据库。
一级缓存在下面情况会被清除:
<setting name="localCacheScope" value="STATEMENT"/>`
localCacheScope,用于控制一级缓存的级别,该参数的取值为SESSION、STATEMENT。当指定localCacheScope参数值为SESSION时,缓存对整个SqlSession有效,只有执行DML语句(更新语句)时,缓存才会被清除。当localCacheScope值为STATEMENT时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。
当创建了一个SqlSession对象时,MyBatis会为这个SqlSession对象创建一个新的Executor执行器,而缓存信息就被维护在这个Executor执行器中,MyBatis将缓存和对缓存相关的操作封装成了Cache接口中。SqlSession、Executor、Cache之间的关系图示:
Executor接口的实现类BaseExecutor中,拥有一个Cache接口的实现类 PerpetualCache,对于BaseExecutor对象而言,它将使用PerpetualCache对象维护缓存。
PerpetualCache实现原理其实很简单,其内部就是通过一个简单的HashMap
key:MapperID+offset+limit+Sql+所有的入参
value:查到的数据。
同一个sqlsession再次发出相同的sql,就从缓存中取出数据。如果两次中间出现commit操作(修改、添加、删除),本sqlsession中的一级缓存区域全部清空,下次再去缓存中查询不到所以要从数据库查询,从数据库查询到再写入缓存。
1)对于某个查询,根据statementId、params、rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果;
2)判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;
3)如果命中,则直接将缓存结果返回;
4)如果没命中:
- 去数据库中查询数据,得到查询结果;
- 将key和查询到的结果分别作为key,value对存储到Cache中;
- 将查询结果返回。
图示:
怎样判断某两次查询是完全相同的查询?也就是说:如何确定Cache中的key值?MyBatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:
也就是说:Cache Key由以下条件决定: statementId、rowBounds、传递给JDBC的SQL、传递给JDBC的参数值。
一个SqlSession对象会使用一个Executor对象来完成会话操作,MyBatis的二级缓存机制的关键就是对这个Executor对象做文章。如果用户配了"cacheEnabled=true",那么MyBatis在为SqlSession对象创建Executor对象时,会对Executor对象加上一个装饰者:CachingExecutor,这时SqlSession使用CachingExecutor对象来完成操作请求。
CachingExecutor对于查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的Executor对象来完成查询操作,之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中,然后在返回给用户。
二级缓存是全局作用域缓存(Mapper级别的缓存),默认是不开启的,需要手动进行配置。二级缓存实现的时候要求实体类实现Serializable接口,二级缓存在sqlSession关闭或提交之后才会生效。
二级缓存是指可以跨 SqlSession 的缓存。是 mapper 级别的缓存,对于 mapper 级别的缓存不同的 sqlsession 是可以共享的。
MyBatis的二级缓存设计得比较灵活,你可以使用MyBatis自己定义的二级缓存实现;你也可以通过实现org.apache.ibatis.cache.Cache接口自定义缓存;也可以使用第三方内存缓存库。
当开一个会话时,一个SqlSession对象会使用一个Executor对象来完成会话操作,MyBatis的二级缓存机制的关键就是对这个Executor对象做文章 。如果用户配置了"cacheEnabled=true",那么MyBatis在为SqlSession对象创建Executor对象时,会对Executor对象加上一个装饰者:CachingExecutor,这时SqlSession使用CachingExecutor对象来完成操作请求。
CachingExecutor对于查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的Executor对象来完成查询操作,之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中,然后再返回给用户。
CachingExecutor是Executor的装饰者,以增强Executor的功能,使其具有缓存查询的功能,这里用到了设计模式中的装饰者模式。
- Mybatis 全局配置中启用二级缓存配置
- 在对应的 Mapper.xml 中配置 cache 节点
- 在对应的 select 查询节点中添加 useCache=true
MyBatis并不是简单地对整个Application,只有一个Cache缓存对象。它将缓存划分的更细,是Mapper级别的,即每一个Mapper都可以拥有一个Cache对象,具体:
a.为每一个Mapper分配一个Cache缓存对象(使用
节点配置);
b.多个Mapper共用一个Cache缓存对象(使用节点配置);
对于每一个Mapper.xml,如果在其中使用了
节点,则MyBatis会为这个Mapper创建一个Cache缓存对象,图示:
上述的每一个Cache对象,都会有一个自己所属的namespace命名空间,并且会将Mapper的namespace作为它们的ID。
如果想让多个Mapper公用一个Cache的话,你可以使用
要开启二级缓存总开关,需要进行3个步骤的配置:
<settings name="cacheEnabled" value="true"/>
<cache />
标签的效果:
- 映射语句文件中的所有 select 语句的结果将会被缓存。
- 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
- 缓存会使用最近最少使用算法(LRU)算法来清除不需要的缓存。
- 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
- 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
- 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
经过以上三步配置,就开启了该mapper.xml的二级缓存。二级缓存是mapper级别的缓存,粒度比一级缓存大,多个SqlSession可以共享同一个mapper的二级缓存。注意开启二级缓存后,SqlSession需要提交,查询的数据才会被刷新到二级缓存当中。
MyBatis对二级缓存的支持粒度很细,它会指定某一条查询语句是否使用二级缓存。虽然在Mapper中配置了
,并且为此Mapper分配了Cache对象,这并不表示使用Mapper中定义的查询语句查到的结果都会放置到Cache对象之中,我们必须指定Mapper中的某条选择语句是否支持缓存,即如下所示,在
节点中配置useCache="true"
,Mapper才会对此Select的查询支持缓存特性,否则,不会对此Select查询,不会经过Cache缓存。
在二级缓存使用时,有一些属性:
二级缓存跟一级缓存中不会同时存在数据
,因为二级缓存中的数据是在sqlsession关闭之后才生效的。缓存查询的顺序是先查询二级缓存再查询一级缓存
。
在开启了完全局的二级缓存后,还可以在每一个单独的select语句进行特殊的缓存设置:
<setting name="cacheEnabled" value="true"/>
一级缓存指的是sqlsession(回话)级别的缓存,关闭回话之后自动失效,默认情况下是开启的。会失效的情况:
二级缓存:表示的是全局缓存,必须要等到sqlsession关闭之后才会生效
(这意味着二级缓存跟一级缓存中不会同时存在数据,因为二级缓存中的数据是在sqlsession关闭之后才生效的)。
要想使某条Select查询支持二级缓存,需要进行如下设置:
<setting name="cacheEnabled" value="true"/>
<cache>cache>
useCache=true
。 如果MyBatis使用了二级缓存,并且Mapper和select语句也配置使用了二级缓存。那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:先查二级缓存,再查一级缓存,再查数据库
。
即使在一个sqlSession中,也会先查二级缓存;一个namespace中的查询更是如此。
一集缓存和二级缓存的具体查询顺序:
Ehcache,这是一个Java进程内的缓存框架。
Ehcache的主要特性:
快速;
简单;
多种缓存策略;
缓存数据有内存和磁盘两级,无须担心容量问题;缓存数据会在虚拟机重启的过程中写入磁盘;
可以通过RMI、可插入API等方式进行分布式缓存;
具有缓存和缓存管理器的监听接口;
支持多级缓存管理器实例以及一个实例的多个缓存区域。