Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

java工具——持久层工具mybatis

mybatis简介

MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github

iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAOs)

mybatis作用

  1. MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。
  2. MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
  3. MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。

持久化


将程序数据在持久状态和瞬间状态之间转化的机制,最好的例子就是将内存中的数据保存到磁盘就是一个持久化的过程

我们的程序在运行时说的持久化通常就是指将内存数据存在硬盘,比如保存在数据库文件、xml文件中

持久层


java中所说的持久层,大体上指将业务中操作数据库的代码统一抽离出来,形成了位于业务层和数据库之间的独立的层

常见的解决方案有:

  • JDBC
  • EJB
  • JDO

mybatis优点和缺点


  • sql语句与代码分离,存放在xml配置文件中
    • 优点:便于维护管理,不用在java代码中找这些语句,使项目能更好的符合开闭原则
    • 缺点:JDBC方式可以用打断点的方式调试,但是mybatis不能,需要通过log4j日志输出日志信息帮助调试,然后再配置文件中修改
  • 用逻辑标签控制动态SQL的拼接
    • 优点:用标签代替写逻辑代码
    • 缺点:拼接复杂SQL语句时,没有代码灵活,拼写比较复杂。
  • 查询的结果集与java对象自动映射
    • 优点:保证名称相同,配置好映射关系即可自动映射,或者,不配置映射关系,通过配置列名 = 字段名也可以完成自动映射
    • 缺点:对开发人员缩写的SQL依赖很强
  • 编写原生SQL
    • 接近JDBC,比较灵活
    • 对SQL语句依赖程度很高;并且属于半自动,数据库移植比较麻烦,比如mysql数据库变成Oracle数据库,部分sql语句需要调整

代理设计模式

代理模式分为:

  1. 静态代理
  2. 动态代理

代理的核心功能是方法增强

静态代理


角色分析


  1. 抽象角色:一般使用接口或者抽象类来实现
  2. 真实角色:被代理的角色
  3. 代理角色:代理真实角色;代理真实角色后,一般会做一些附属的操作
  4. 客户:使用代理角色来进行一些操作

例子


写一个接口

1
2
3
4
5
6
public interface Singer {
/**
* 歌手都能唱歌
*/
public void sing();
}

男歌手

1
2
3
4
5
6
7
8
9
10
11
12
public class MaleSinger implements Singer{
private String name;

public MaleSinger(String name) {
this.name = name;
}

@Override
public void sing(){
System.out.println(this.name+"开始唱歌了");
}
}

经纪人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Agent implements Singer{

private Singer singer;

public Agent(Singer singer) {
this.singer = singer;
}

@Override
public void sing() {
System.out.println("和节目组谈好价格,曲目,场地....");
singer.sing();
System.out.println("结算费用");
}
}

客户

1
2
3
4
5
6
7
public class Client {
public static void main(String[] args) {
Singer singer = new MaleSinger("Bob.M");
Singer agent = new Agent(singer);
agent.sing();
}
}

这个过程中我们直接接触的是经纪人,经济人在演出前后做了一些复杂的事情

优点

  • 虽然整个演出的过程复杂了,但是歌手类并没有改变
  • 公共的统一问题都交给了代理完成
  • 公共业务进行拓展或变更时,可以更为方便
  • 符合开闭原则、单一原则
缺点

  • 每个类都写个代理,重复工作较多,比较麻烦

动态代理


  1. 动态代理的角色和静态代理的一样
  2. 动态代理的代理类是动态生成的,静态代理中的代理是我们自己创建的
  3. 动态代理分为两类:
    1. 基于接口的动态代理——JDK动态代理
    2. 基于类的动态代理——cglib
  4. 现在用的比较多的是javasist来生成动态代理

JDK动态代理


核心:invocationHandler和Proxy,打开JDK帮助文档可查看

【invocationHandler:调用处理程序】

1
2
3
4
5
Object invoke(Object proxy,Method method, Object[] args);
//参数
//proxy -调用该方法的代理实列
//method - 所描述方法对应于调用代理实列上的接口方法的实列。方法对象的声明类将是该方法声明的接口,它可以是代理类继承该方法的代理接口的超级接口
//args -包含的方法调用传递代理实列的参数值的对象的阵列,或null如果接口方法没有参数。原始类型的参数包含在适当的原始包装器类的实列中,例如java.lang.Integer或java.lang.Boolean.

【proxy:代理】

1
2
3
4
5
6
7
//生成代理类
public Object getProxy(){
return Proxy.new ProxyInstance(this.getClass().getClassLoader(), rent.getClass().getInterface(),this);
}
/**参数:
* 类加载器,接口数组,innovationHandler
*/

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Client {
public static void main(String[] args) {
Singer singer = new MaleSinger("Bob.M");
Singer agent = (Singer) Proxy.newProxyInstance(Client.class.getClassLoader(), singer.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("节目组找过了,需要演出,谈好费用...");
Object invoke = method.invoke(singer, args);
System.out.println("结算费用,下一次合作预约...");
return invoke;
}
});
//此处匿名内部类InvocationHandler也可以使用lambda表达式:
// Singer agent = (Singer) Proxy.newProxyInstance(Client.class.getClassLoader(), singer.getClass().getInterfaces(), (proxy, method, args1) -> {
// System.out.println("节目组找过了,需要演出,谈好费用...");
// Object invoke = method.invoke(singer, args1);
// System.out.println("结算费用,下一次合作预约...");
// return invoke;
// });

agent.sing();
}
}

Proxy会利用java发反射机制,自动的为你的项目中创建一个代理类,使用如下语句,生成的动态代理,就会在编译后的文件中显示

1
2
3
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles",true);//jdk1.8可以使用
//然而在新版本中使用该方法仍然无法显示,通过查找包中ProxyGenerator类的路径得知,新版本需要如下设置
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles","true");//jdk11可以使用

原因在此:

Singer代理产生类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Agent implements InvocationHandler {

private Object object;

public Agent(Object object) {
this.object = object;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

System.out.println("节目组找过了,需要演出,谈好费用...");
Object invoke = method.invoke(object, args);
System.out.println("结算费用,下一次合作预约...");
return invoke;
}

public static Object getProxy(Singer singer){
Agent agent = new Agent(singer);

return Proxy.newProxyInstance(Agent.class.getClassLoader(),singer.getClass().getInterfaces(),agent);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
public static void main(String[] args) {
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles","true");

Singer singer = new MaleSinger("Bob.M");

Singer agent = (Singer) Agent.getProxy(singer);

agent.sing();

Singer singer1 = new MaleSinger("Bob.D");
Singer agent1 = (Singer) Agent.getProxy(singer1);
agent1.sing();
}
}

万能代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Agent implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("打开资源");
System.out.println(method.getName());
if(args != null){
System.out.println(args[0].getClass().getSimpleName());
}
System.out.println("关闭资源");
return null;
}

public static <T>T getProxy(Class<T> target){
Agent agent = new Agent();
Object o = Proxy.newProxyInstance(Agent.class.getClassLoader(), new Class[]{target}, agent);//new 一个该类型的接口
return (T)o;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) {
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles","true");

Singer proxy = Agent.getProxy(Singer.class);
proxy.sing();


IUserDao proxy1 = Agent.getProxy(IUserDao.class);
proxy1.saveUser("Ender");
}
}

搭建环境

建立数据库


1
2
3
4
5
6
7
8
9
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` INT(20) NOT NULL,
`username` VARCHAR(30) DEFAULT NULL,
`password` VARCHAR(30) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT into `user`(`id`,`username`,`password`)VALUE (1,'张三','123456'),(2,'李四','abcdef'),(3,'王五','987654');

编写实体类


此处用到lombok插件,可使用注释的方式在编译时生成构造函数,toString等方法

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author Ender-PC
* @date 2021/2/2
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private int id;
private String username;
private String password;
}

Maven配置


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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>

<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.2</version>
<scope>test</scope>
</dependency>
<!--mybatis 核心-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
</dependencies>
<!--单元测试-->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>11</source>
<target>11</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
<!--防止在java中写的配置文件不被加载到resources文件夹-->
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>

Mybatis核心配置文件


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
<?xml version="1.0" encoding="UTF-8" ?>
<!--对xml的约束-->
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 1.0//EN"
"http://mybatis.org/schema/mybatis-3-config.dtd">
<configuration>
<!--环境-->
<environments default="development">
<environment id="development">
<!--事物管理器-->
<transactionManager type="JDBC"/>
<!--数据源-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/ssm?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>

<mappers>
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
</configuration>

实列对应的xml,在该文件中,可以直接添加sql语句来进行查询

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" ?>
<!--对xml的约束-->
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ender.dao.UserMapper">
<select id="selectUsers" resultType="com.ender.entity.User">
select id,username,password from user
</select>
</mapper>

DAO层接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author Ender-PC
*/
public interface UserMapper {
/**
* 查询所有用户信息
* @return
*/
List<User> selectUsers();

/**
* 根据id查找用户
* @param id
* @return
*/
User selectUserById(int id);
}

测试

此处使用junit中的before和after将数据库的开关分离处理

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
/**
* @author Ender-PC
* @date 2021/2/2
*/
public class TestUser {

private SqlSession session;

@Before
public void before(){
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
session = sqlSessionFactory.openSession();

} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void testSelectUsers(){
//使用代理调用dao
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> users = mapper.selectUsers();
for (User user : users) {
System.out.println(user);
}
}

@After
public void after(){
session.commit();
session.close();
}
}

CRUD

select


  1. 在接口中添加方法的声明:
1
2
3
4
5
6
7
8
public interface UserMapper {
/**
* 根据id查找用户
* @param id
* @return
*/
User selectUserById(int id);
}
  1. 在UserMapper.xml中添加select语句
1
2
3
<select id="selectUserById" resultType="com.ender.entity.User">
select id,username,password from user where id = #{id}
</select>

其中:

  1. resultType:指定了返回值类型
  2. parameterType:参数类型(具有自动映射自动转化的过程)
  3. id:指定对应的方法
  4. #{id}:sql中的变量,要保证大括号中的变量必须在User对象里有
  5. #{}:占位符,其实就是PreparedStatement处理这个变量

#{}与${}的区别


  • #{}的主要作用就是替换预编译语句{PreparedStatement}中的占位符, ?[推荐使用]
1
2
INSERT INTO user (name) VALUE (#{name});
INSERT INTO user (name) VALUE (?);
  • ${}的作用是直接进行字符串替换
1
2
INSERT INTO user (name) VALUE ('${name}');
INSERT INTO user (name) VALUE ('赵六');

insert


1
2
3
4
5
6
7
8
9
10
11
/**
* @author Ender-PC
*/
public interface UserMapper {
/**
* 保存用户信息
* @param user
* @return
*/
int saveUser(User user);
}
1
2
3
<insert id="saveUser" parameterType="com.ender.entity.User">
insert into user (username,password) value (#{username},#{password})
</insert>

update


1
2
3
4
5
6
7
8
9
10
11
/**
* @author Ender-PC
*/
public interface UserMapper {
/**
* 更新用户信息
* @param user
* @return
*/
int updateUser(User user);
}
1
2
3
<update id="updateUser" parameterType="com.ender.entity.User">
update user set username = #{username} , password = #{password} where id = #{id}
</update>

delete


1
2
3
4
5
6
7
8
9
10
11
/**
* @author Ender-PC
*/
public interface UserMapper {
/**
* 根据id删除一个用户记录
* @param id
* @return
*/
int deleteUser(int id);
}
1
2
3
<delete id="deleteUser" parameterType="java.lang.Integer">
delete from user where id = #{id}
</delete>

操作注释

通常不推荐使用注释的方法,因为要保证代码遵循开闭原则,如果使用注释操作,今后修改时就需要修改源代码,违反了开闭原则

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* @author Ender-PC
* @date 2021/2/2
*/
public class TestAdmin {

private SqlSession session;

@Before
public void before(){
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
session = sqlSessionFactory.openSession();

} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void testSelectAdmins(){
//使用代理调用dao
AdminMapper mapper = session.getMapper(AdminMapper.class);
List<Admin> admins = mapper.selectAdmins();
for (Admin admin : admins) {
System.out.println(admin);
}
}

@Test
public void testSelectAdminsById(){
AdminMapper mapper = session.getMapper(AdminMapper.class);
Admin admin = mapper.selectAdminById(1);
System.out.println(admin);
}

@Test
public void testSaveAdmin(){
AdminMapper mapper = session.getMapper(AdminMapper.class);
Admin admin = new Admin(4, "赵六", "123");
int rows = mapper.saveAdmin(admin);
System.out.println(rows);
}

@Test
public void testUpdateAdmin(){
AdminMapper mapper = session.getMapper(AdminMapper.class);
Admin admin = new Admin(4, "赵六2", "dsjioa123");
int rows = mapper.updateAdmin(admin);
System.out.println(rows);
}

@Test
public void testDeleteAdmin(){
AdminMapper mapper = session.getMapper(AdminMapper.class);
int rows = mapper.deleteAdmin(4);
System.out.println(rows);
}

@After
public void after(){
session.commit();
session.close();
}
}

注册时需要选择类进行注册

1
2
3
<mappers>
<mapper class="com.ender.dao.AdminMapper"/>
</mappers>

模糊查找

Java代码中拼接字符串


1
2
3
4
string name = "%IT%";
//对于带有索引的字段,建议使用如下形式
//string name = "IT%";
list<name> names = mapper.getUserByName(name);
1
2
3
<select id = "getUserByName">
select * from user where name like #{name}
</select>

在配置文件中拼接


1
2
string name = "IT";
list<name> names = mapper.getUserByName(name);
1
2
3
4
<select id="">
select * from user where name like "%"#{name}"%"
</select>
<!--此处必须用双引号-->

map的使用

map可以代替任何的实体类,所以当我们数据比较复杂时,可以适当考虑使用map来完成相关工作

  1. 配置文件
1
2
3
<select id="getUserByParams" resultType="map" >
select id,username,password from user where username = #{name}
</select>
  1. 方法
1
2
3
4
5
6
/**
* 根据一些参数查询
* @param map
* @return
*/
List<User> getUsersByParams(Map<String,String> map);
  1. 测试
1
2
3
4
5
6
7
8
9
10
@Test
public void findByParams(){
UserMapper mapper = session.getMapper(UserMapper.class);
Map<String,String> map = new HashMap<String,String>();
map.put("name","郑七");
Lisr<User> users = mapper.getUsersByparams(map);
for(User user: users){
System.out.println(user.getUsername());
}
}

多个参数

对于单个参数的方法来说,mybatis将自动将参数对应到sql语句中,但若参数包含多个,可以使用如下方式

1
2
3
4
5
6
7
/**
* 通过字符串添加记录
* @param username
* @param password
* @return
*/
int saveAdmin(@Param("username") String username,@Param("password") String password);
1
2
3
<select id="">
insert into admin (username,password) values (#{username},#{password})
</select>

使用多参数时,需要使用注释表明对应关系,其次还可以使用一个map传参,mybatis将通过key、value来处理关系

别名

内置别名


mybatis内置别名:

Alias Mapped Type
_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 Obkect
map Map
hashmap HashMap
list List
arraylist ArrayList
collection Collection
iterator Iterator

自定义别名


在核心配置文件中加入

1
2
3
<typeAliases>
<typeAlias type="com.ender.entity.User" alias="user"/>
</typeAliases>

<typeAliases>标签需要在<enviroment>之前,基本顺序为:<properties><settings>typeAliastypeHandlers、、、、<environment>

<typeAlias>标签中有typealice两个属性

type填写实体类的全类名,alias可以不填,不填默认为类名,不区分大小写,alias有值时以其中的值为准

1
2
3
<typeAliases>
<package name="com.ender.entity" />
</typeAliases>

<package>标签为某个包下所有类起别名;name属性填写包名。包名默认是类名,不区分大小写

1
@Alias() 注解 加在实体类上,为某个类起别名:例如`@Alias("User")`

mybatis配置文件

mybatis的配置文件分为:

  1. 核心配置文件
  2. mapper配置文件

核心配置文件


  • mybatis-config.xml系统核心配置文件
  • 核心配置文件主要配置mybatis一些基础组件和加载资源,核心配置文件中的元素常常能影响mybatis的整个运行过程
  • 能配置的内容如下,顺序不能乱:
1
2
3
4
5
6
7
8
9
10
11
12
1. properties是一个配置属性的元素
2. settings设置,mybatis最为复杂的配置也是最重要的,会改变mybatis运行时候的行为
3. typeAliases别名(在Type AliasRegistry中可以看到mybatis提供了许多的系统别名)
4. typeHandlers 类型处理器(比如在预处理语句中设置一个参数或者从结果集中获取一个参数的时候,都会用到类型处理器,在TypeHandlerRegistry中定义了很多的类型处理器)
5. objectFactory 对象工厂 (mybatis在构建一个和结构或返回的时候,会使用一个objectFactory去构建pojo)
6. plugins 插件
7. environments 环境变量
1. environment 环境变量
1. transactionManager 事务管理器
2. dataSource 数据源
3. databaseIdProvider 数据库厂商标识
8. mappers 映射器

environments元素


为mybatis配置多环境运行,将SQL映射到多个不同的数据库上,必须指定其中一个为默认运行环境(通过default指定),如果想切换环境修改default的值即可

最常见的就是,生产环境和开发环境,两个环境切换必将导致数据库的切换

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
<environments default="development">
<environment id="development">
<!--事物管理器-->
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<!--数据源-->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>

<environment id="product">
<!--事物管理器-->
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<!--数据源-->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
  • dataSource 元素使用标准的JDBC数据源接口来配置JDBC连接对象的资源

  • 数据源是必须配置的

  • 有三种内建的数据源类型

1
2
3
4
type="[UNPOOKED|POOKED|JNDI]"
- unpooked:这个数据源的实现只是每次被请求时打开和关闭连接
- pooled:这种数据源的实现利用“池”的概念将JDBC连接对象组织起来,这是一种使得并发Web应用快速响应请求的流行处理方法
- jndi:这个数据源实现时为了能在如spring或应用服务器中使用,容器可以集中或在外部配hi数据源,然后放置一个JNDI上下文引用
  • 数据源也有很多第三方的实现,比如druid,dbcp,c3p0等等…

  • 这两种事物管理器类型都不需要设置任何属性

  • 具体的一套环境,通过设置id进行区别,id保证唯一

  • 子元素节点:transactionManager - [事物管理器]

1
2
<!-- 语法 -->
<transactionManager type="[JDBC | MANAGED]" />
  • 子元素节点:数据源(dataSource)

mappers元素


对写好的mapper和xml进行统一管理

引入方式

1
2
3
4
5
6
7
<mappers>
<!-- 使用相对于类路径的资源引用 -->
<mapper resource="com/ender/dao/userMapper.xml"/>
<!-- 面向注解时使用全类名 -->
<mapper class="com.ender.dao.AdminMapper"/>
</mappers>
<!--等等其他方式-->

Mapper文件

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xinzhi.mapper.UserMapper">
</mapper>
  • namespace:命名空间
  • namespace的命名必须跟某个接口同名

Properties元素


数据库连接信息我们最好放在一个单独的文件中。

  • 在资源目录下新建一个db.properties
1
2
3
4
5
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/ssm?
useSSL=true&useUnicode=true&characterEncoding=utf8
username=root
password=root
  • 导入properties配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<configuration>
<!--导入properties文件-->
<properties resource="db.properties"/>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>

设定别名


详见别名

其他配置浏览


settings能对我的一些核心功能进行配置,如懒加载、日志实现、缓存开启关闭等

简单参数说明:

点击查看
设置参数描述有效值默认值
cacheEnabled该配置影响的所有映射 器中配置的缓存的全局 开关。true | falsetrue
lazyLoadingEnabled延迟加载的全局开关。 当开启时,所有关联对 象都会延迟加载。特定 关联关系中可通过设置 fetchType属性来覆盖该 项的开关状态。true | falsefalse
useColumnLabel使用列标签代替列名。 不同的驱动在这方面会 有不同的表现,具体可 参考相关驱动文档或通 过测试这两种不同的模 式来观察所用驱动的结 果。true | falsetrue
useGeneratedKeys允许 JDBC 支持自动生 成主键,需要驱动兼 容。如果设置为 true 则 这个设置强制使用自动 生成主键,尽管一些驱 动不能兼容但仍可正常 工作(比如 Derby)。true | falseFalse
defaultStatementTimeout设置超时时间,它决定 驱动等待数据库响应的 秒数。Any positive integer Not SNot Set (null)
mapUnderscoreToCamelCase是否开启自动驼峰命名 规则(camel case)映 射,即从经典数据库列 名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。true | falseFalse
logPrefix指定 MyBatis 增加到日 志名称的前缀。Any StringNot set
logImpl指定 MyBatis 所用日志 的具体实现,未指定时 将自动查找。SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGINGNot set
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<settings>
<!---->
<setting name="cacheEnabled" value="true"/>
<!---->
<setting name="lazyLoadingEnabled" value="true"/>
<!---->
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
<setting name="defaultFetchSize" value="100"/>
<setting name="safeRowBoundsEnabled" value="false"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
<setting name="lazyLoadTriggerMethods"
value="equals,clone,hashCode,toString"/>
</settings>

动态sql

概述


MyBatis提供了对SQL语句动态的组装能力,大量的判断都可以在 MyBatis的映射XML文件里面配置,以达到许多我们需要大量代码才能实现的功能,大大减少了我们编写代码的工作量。 动态SQL的元素

动态SQL中的元素

元素 作用 备注
if 判断语句 但条件分支判断
choose、when、otherwise 相当于java中的case when语句 多条件分支判断
trim、where、set 辅助元素 用于处理一些SQL拼装问题
foreach 循环语句 在in语句等列举调价常用

if元素(常用)


if元素相当于Java中的if语句,它常常与test属性联合使用。现在我们要根据username去查询用户,但是username是可选的,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="findUserById" resultType="com.ender.entity.User">
select id,username,password from user
where 1 =1
<if test="id != null and id != ''">
AND id = #{id}
</if>
<if test="username != null and username != ''">
AND username = #{username}
</if>
<if test="password != null and password != ''">
AND password = #{password}
</if>
</select>

choose、when、otherwise元素


有些时候我们还需要多种条件的选择,在Java中我们可以使用switch、case、default语句,而在映射器 的动态语句中可以使用choose、when、otherwise元素

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 有name的时候使用name搜索,没有的时候使用id搜索 -->
<select id="select" resultType="com.ender.entity.User">
SELECT * FROM user
WHERE 1=1
<choose>
<when test="name != null and name != ''">
AND username LIKE concat('%', #{username}, '%')
</when>
<when test="id != null">
AND id = #{id}
</when>
</choose>
</select>

where元素


上面的select语句我们加了一个 1=1 的绝对true的语句,目的是为了防止语句错误,变成 SELECT * FROM student WHERE 这样where后没有内容的错误语句。这样会有点奇怪,此时可以使用where元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="findUserById" resultType="com.xinzhi.entity.User">
select id,username,password from user
<where>
<if test="id != null and id != ''">
AND id = #{id}
</if>
<if test="username != null and username != ''">
AND username = #{username}
</if>
<if test="password != null and password != ''">
AND password = #{password}
</if>
</where>
</select>

trim元素


有时候我们要去掉一些特殊的SQL语法,比如常见的and、or,此时可以使用trim元素。trim元素意味着 我们需要去掉一些特殊的字符串,prefix代表的是语句的前缀,而prefixOverrides代表的是你需要去掉 的那种字符串,suffix表示语句的后缀,suffixOverrides代表去掉的后缀字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<insert id="saveUser" parameterType="com.ender.entity.User">
insert into user
(
<trim suffixOverrides=",">
<if test="username != null and username != ''">
username,
</if>
<if test="password != null and password != ''">
password,
</if>
</trim>
)value(
<trim suffixOverrides=",">
<if test="username != null and username != ''">
#{username},
</if>
<if test="password != null and password != ''">
#{password},
</if>
</trim>
)
</insert>

set元素


在update语句中,如果我们只想更新某几个字段的值,这个时候可以使用set元素配合if元素来完成。注 意:set元素遇到,会自动把,去掉。

1
2
3
4
5
6
7
8
9
10
11
12
<update id="updateUser" parameterType="com.ender.entity.User">
update user
<set>
<if test = "username != null and username != ''">
username = #{username}
</if>
<if test="password != null and password != ''">
password = #{password}
</if>
</set>
where id = #{id}
</update>

foreach元素


foreach元素是一个循环语句,它的作用是遍历集合,可以支持数组、List、Set接口。

1
2
3
4
5
6
7
<select id="select" resultType="com.xinzhi.entity.User">
SELECT * FROM user
WHERE id IN
<foreach collection="ids" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</select>
  • collection配置的是传递进来的参数名称
  • item配置的是循环中当前的元素。
  • index配置的是当前元素在集合的位置下标。
  • open和 close配置的是以什么符号将这些集合元素包装起来。
  • separator是各个元素的间隔符。

SQL片段


有时候可能某个 sql 语句我们用的特别多,为了增加代码的重用性,简化代码,我们需要将这些代码抽 取出来,然后使用时直接调用。

1
2
3
<sql id="user-all-content">
select id,username,password from user
</sql>

引用

1
<include refid="user-all-content"/>

结果映射resultMap

数据库不可能永远是你所想或所需的那个样子

属性名和字段名不一致,我们一般都会按照约定去设计数据的,但确实阻止不了沟通不充分等问题

  1. java中实体类的设计
1
2
3
4
5
6
7
8
public class User {
private int id; //id
private String name; //姓名,数据库为username
private String password; //密码,一致
//构造
//set/get
//toString()
}
  1. mapper类
1
2
//根据id查询用户
User selectUserById(int id);
  1. mapper映射文件
1
2
3
<select id="selectUserById" resultType="user">
select * from user where id = #{id}
</select>
  1. 测试
1
2
3
4
5
6
7
@Test
public void testSelectUserById() {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectUserById(1);
System.out.println(user);
session.close();
}

结果


  • User{id=1,name=’null’,password=’123’}
  • 查询出来发现name为空

分析


  • select * from user where id = #{id} 可以看做 select id,username,password from user where id = #{id}
  • mybatis会根据这些查询的列名(会将列名转化为小写,数据库不区分大小写) , 利用反射去对应的实体类中查找相应列名的set方法设值 ,当然找不到username

解决方法


方案一


为列名指定别名,别名和java实体类的属性名一致

1
2
3
<select id="selectUserById" resultType="User">
select id , username as name ,password from user where id = #{id}
</select>

方案二


使用结果集映射$\to$ResultMap(推荐)

1
2
3
4
5
6
7
8
9
10
11
<resultMap id="UserMap" type="User">
<!-- id为主键 -->
<id column="id" property="id"/>
<!-- column是数据库表的列名 , property是对应实体类的属性名 -->
<result column="username" property="name"/>
<!-- id表示表中的主键,需要独立设置,result表示其他字段-->
<result column="password" property="password"/>
</resultMap>
<select id="selectUserById" resultMap="UserMap">
select id , username , password from user where id = #{id}
</select>

常用映射


通常在数据库中我们会使用下划线的命名方式,而java中通常使用驼峰命名法,mybatis为我们提供了两者的自动转化

1
2
3
4
<settings>
<!--开启驼峰命名规则-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

日志配置

配置日志的一个重要原因是想在调试的时候能观察到sql语句的输出,能查看中间过程

标准日志实现


指定 MyBatis 应该使用哪个日志记录实现。如果此设置不存在,则会自动发现日志记录实现。

STD:standard out:输出

STDOUT_LOGGING:标准输出日志

1
2
3
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

组合log4j完成日志功能


  1. 导入log4j
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.30</version>
</dependency>
  1. 配置文件编写log4j.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代

log4j.rootLogger=DEBUG,console,file
#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%c]-%m%n
#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/xinzhi.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n
#日志输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
  1. setting设置日志实现
1
2
3
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
  1. 在程序中使用Log4j进行输出
1
2
3
4
5
6
7
8
@Test
public void findAllUsers() {
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> users = mapper.selectUser();
for (User user: users){
System.out.println(user);
}
}

多表查询

数据库设计


  • 部门和员工的关系为:一个部门多个员工,一个员工属于一个部门
  • 依次我们可以实际一个一对多的数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE `dept` (
`id` INT(10) NOT NULL,
`name` VARCHAR(30) DEFAULT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO dept VALUES (1, 'ender开发六部');
CREATE TABLE `employee` (
`id` INT(10) NOT NULL,
`name` VARCHAR(30) DEFAULT NULL,
`did` INT(10) DEFAULT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_did` FOREIGN KEY (`did`) REFERENCES `dept` (`id`)
);
INSERT INTO employee VALUES (1, '小兰', 1);
INSERT INTO employee VALUES (2, '小红', 1);
INSERT INTO employee VALUES (3, '小白', 1);
INSERT INTO employee VALUES (4, '小黑', 1);
INSERT INTO employee VALUES (5, '小黄', 1);

在多的一方维护关系


方法一:结果集嵌套


编写实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @author Ender-PC
* @date 2021/2/6
*/
@Data
public class Dept implements Serializable {
private int id;
private String name;
}


/**
* @author Ender-PC
* @date 2021/2/6
*/
@Data
public class Employee implements Serializable {
private int id;
private String name;
/**
* 外键Dept,维护关系
*/
private Dept dept;
}
编写实体类对应的Mapper接口

1
2
3
4
public interface DeptMapper {
}
public interface EmployeeMapper {
}
编写mapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ender.mapper.EmployeeMapper">
</mapper>


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ender.mapper.DeptMapper">
</mapper>
编写方法

1
2
3
4
5
6
/**
* 按id获取指定员工信息
* @param id
* @return employee
*/
Employee findEmployeesById(Integer id);
mapper处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ender.mapper.EmployeeMapper">
<resultMap id="EmployeeDept" type="com.ender.entity.Employee">
<!--起别名后需要设置映射关系-->
<id property="id" column="eid" />
<result property="name" column="ename" />
<!--指定包含类的映射关系-->
<association property="dept" javaType="com.ender.entity.Dept">
<id property="id" column="did" />
<result property="name" column="dname" />
</association>
</resultMap>

<select id="findEmployeesById" parameterType="int" resultMap="EmployeeDept">
SELECT e.id eid, e.name ename,d.id did,d.name dname
FROM employee e
LEFT JOIN dept d
on e.did = d.id where e.id = #{id}
</select>
</mapper>
注册Mapper

1
2
<mapper resource="mappers/DeptMapper.xml" />
<mapper resource="mappers/employeeMapper.xml" />
测试

1
2
3
4
5
6
@Test
public void testFindEmployessById(){
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
Employee employee = mapper.findEmployeesById(1);
System.out.println(employee);
}

方法二:查询嵌套


编写mapper.xml

employeeMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ender.mapper.EmployeeMapper">
<resultMap id="EmployeeDept" type="com.ender.entity.Employee">
<!--使用select标签将第二步的查询委托给另外的mapper-->
<association property="dept" javaType="com.ender.entity.Dept" select="com.ender.mapper.DeptMapper.findDeptById" column="did" />
</resultMap>

<select id="findEmployeesById" resultMap="EmployeeDept">
SELECT id,name,did FROM employee WHERE id = #{id}
</select>
</mapper>

deptMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ender.mapper.DeptMapper">
<!--起别名后记得定义映射关系-->
<resultMap id="DeptResult" type="com.ender.entity.Dept">
<id property="id" column="did" />
</resultMap>
<select id="findDeptById" resultMap="DeptResult">
SELECT id did,name FROM dept WHERE id = #{id}
</select>
</mapper>

在一的一方维护关系


即查找所有部门时,将该部门下的所有员工一并查找出来

方法一:结果集嵌套


实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author Ender-PC
* @date 2021/2/6
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Dept implements Serializable {
private int id;
private String name;
/**
* 该部门下的所有员工
*/
List<Employee> employees;
}
方法

1
2
3
4
5
6
7
8
9
10
11
/**
* @author Ender-PC
* @date 2021/2/6
*/
public interface DeptMapper {
/**
* 查找所有部门信息
* @return deptList
*/
List<Dept> findDepts();
}
Mapper配置

DeptMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ender.mapper.DeptMapper">
<resultMap id="DeptEmployees" type="dept">
<id property="id" column="did" />
<result property="name" column="dname" />
<!--一对多时使用collection标签,在映射数组类型时,需要使用ofType属性来指定类型,告知mybatis该List中的元素属于类,而JavaType用于告诉mybatis,映射参数的类型-->
<collection property="employees" ofType="employee">
<id property="id" column="eid"/>
<result property="name" column="ename"/>
</collection>
</resultMap>
<select id="findDepts" resultMap="DeptEmployees">
SELECT d.id did,d.name dname,e.id eid,e.name ename
FROM dept d
LEFT JOIN employee e on d.id = e.did
</select>
</mapper>

方法二:查询嵌套


Mapper配置

DeptMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ender.mapper.DeptMapper">
<!-- 查询嵌套-->
<resultMap id="deptEmpResult" type="dept" >
<id property="id" column="id"/>
<!--代理给Employee下的查询语句查询-->
<collection property="employees" ofType="employee" column="id" select="com.ender.mapper.EmployeeMapper.findEmployeesByDid"/>
</resultMap>

<select id="findDepts" resultMap="deptEmpResult">
SELECT id,name FROM dept
</select>
</mapper>

EmployeeMapper.xml

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ender.mapper.EmployeeMapper">
<select id="findEmployeesByDid" resultType="employee">
SELECT id,name,did FROM employee WHERE did = #{id}
</select>
</mapper>

Mybatis缓存

为什么需要缓存


  • 如果缓存中有数据,就不用从数据库获取,大大提高系统性能
  • mybatis提供一级缓存和二级缓存

一级缓存


一级缓存是sqlsession级别的缓存

  • 操作数据库时,需要构造sqlsession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据
  • 不同的sqlsession之间的缓存区域时互不影响的

一级缓存工作原理


image-20210207163050284

  • 第一次发起查询sql查询用户id为1的用户,先去找缓存中是否有id为1的用户,如果没有,再去数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。
  • 如果sqlsession执行了commit操作(插入,更新,删除),会清空sqlsession中的一级缓存,避免脏读
  • 第二次发起查询id为1的用户,缓存中如果找到了,直接从缓存中获取用户信息 mybatis默认支持并开启一级缓存。

可以通过mybatis写入日志时是否调用了MySQL语句来判断缓存是否被调用

测试


编写接口方法

1
2
//根据id查询用户
User findUserById(@Param("id") int id);
Mapper配置

1
2
3
<select id="findUserById" resultType="com.ender.entity.User">
select * from user where id = #{id}
</select>
测试

1
2
3
4
5
6
7
8
9
10
@Test
public void testFindUserById(){
UserMapper mapper = session.getMapper(UserMapper.class);
User user1 = mapper.findUserById(1);
System.out.println(user1);
User user2 = mapper.findUserById(3);
System.out.println(user2);
User user3 = mapper.findUserById(1);
System.out.println(user3);
}
查看日志

1
2
3
4
5
6
7
8
9
10
11
12
[com.xinzhi.dao.UserMapper.findUserById]-==> Preparing: select
id,username,password from user where id = ?
[com.xinzhi.dao.UserMapper.findUserById]-==> Parameters: 1(Integer)
[com.xinzhi.dao.UserMapper.findUserById]-<== Total: 1
User{id=1, username='楠哥', password='123456'} ---->ID为1,第一次有sql
[com.xinzhi.dao.UserMapper.findUserById]-==> Preparing: select
id,username,password from user where id = ?
[com.xinzhi.dao.UserMapper.findUserById]-==> Parameters: 3(Integer)
[com.xinzhi.dao.UserMapper.findUserById]-<== Total: 1
User{id=3, username='磊哥', password='987654'} ---->ID为3,第一次有sql
User{id=1, username='楠哥', password='123456'} ---->ID为1,第二次无sql,
走缓存
一级缓存失效

  1. sqlSession不同
  2. 当sqlSession对象相同的时候,查询的条件不同,原因是第一次查询时候一级缓存中没有第二次查询所需要的数据
  3. 当sqlSession对象相同,两次查询之间进行了插入的操作
  4. 当sqlSession对象相同,手动清除了一级缓存中的数据

二级缓存


二级缓存时mapper级别的缓存,默认关闭

  • 多个SQL session去操作同一个mapper的sql语句,多个sqlsession可以共用二级缓存,所得到的数据会存在二级缓存区域
  • 二级缓存时跨sqlsession的
  • 二级缓存相比一级缓存的范围更大(按namespace划分),多个sqlsession可以共享一个二级缓存

image-20210208131638840

image-20210208131728065

打开二级缓存


首先要手动开启mybatis二级缓存。 在config.xml设置二级缓存开关 , 还要在具体的mapper.xml开启二级缓存

1
2
3
4
<settings>
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
1
2
<!-- 需要将映射的javabean类实现序列化 -->
class Student implements Serializable{}
1
2
<!--开启本Mapper的namespace下的二级缓存-->
<cache eviction="LRU" flushInterval="100000"/>

cache属性的简介


eviction

回收策略(缓存满了的淘汰机制),目前MyBatis提供以下策略。

  1. LRU(Least Recently Used),最近最少使用的,最长时间不用的对象

  2. FIFO(First In First Out),先进先出,按对象进入缓存的顺序来移除他们

  3. SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象

  4. WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。这里采用的是LRU, 移除最长时间不用的对形象

flushInterval

刷新间隔时间,单位为毫秒

  1. 这里配置的是100秒刷新,如果你不配置它,那么当SQL被执行的时候才会去刷新缓存。
size

引用数目

  1. 一个正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。 这里配置的是1024个对象
readOnly

只读

  1. 意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有办法修改缓存,他的默认值是false,不允许我们修改

操作过程


sqlsession1查询用户id为1的信息,查询到之后,会将查询数据存储到二级缓存中。

如果sqlsession3去执行相同mapper下sql,执行commit提交,会清空该mapper下的二级缓存区域的数据

sqlsession2查询用户id为1的信息,去缓存找是否存在缓存,如果存在直接从缓存中取数据

禁用二级缓存

在statement中可以设置useCache=false,禁用当前select语句的二级缓存,默认情况为true

1
2
<select id="getStudentById" parameterType="java.lang.Integer"
resultType="Student" useCache="false">

在实际开发中,针对每次查询都需要最新的数据sql,要设置为useCache=”false” ,禁用二级缓存

flushCache标签

刷新缓存(清空缓存)

1
2
<select id="getStudentById" parameterType="java.lang.Integer"
resultType="Student" flushCache="true">

一般下执行完commit操作都需要刷新缓存,flushCache=”true 表示刷新缓存,可以避免脏读

脏读:当数据保存在缓存中时,使用修改操作将数据库中的数据修改,此时,缓存中的数据和数据库中的不一样,于是下次读操作若从缓存中读取,则会出现读到的数据不是正确的数据的情况

二级缓存应用场景

对于访问多的查询请求并且用户对查询结果实时性要求不高的情况下,可采用mybatis二级缓存,降低 数据库访问量,提高访问速度,如电话账单查询 根据需求设置相应的flushInterval:刷新间隔时间,比如三十分钟,24小时等。

二级缓存局限性

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

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testFindUserCache() throws Exception {
//使用不同的mapper
UserMapper mapper1 = session.getMapper(UserMapper.class);
User user1 = mapper1.findUserById(1);
System.out.println(user1);
//提交了就会刷到二级缓存,要不还在一级缓存,一定要注意
session.commit();
UserMapper mapper2 = session.getMapper(UserMapper.class);
User user2 = mapper2.findUserById(1);
System.out.println(user2);
System.out.println(user1 == user2);
}
结果

1
2
3
4
5
6
7
8
9
10
11
[com.ender.dao.UserMapper.findUserById]-==> Preparing: select
id,username,password from user where id = ?
[com.ender.dao.UserMapper.findUserById]-==> Parameters: 1(Integer)
[com.ender.dao.UserMapper.findUserById]-<== Total: 1
User{id=1, username='ender', password='123456'}
[com.ender.dao.UserMapper.findUserById]-==> Preparing: select
id,username,password from user where id = ?
[com.ender.dao.UserMapper.findUserById]-==> Parameters: 1(Integer)
[com.ender.dao.UserMapper.findUserById]-<== Total: 1
User{id=1, username='ender', password='123456'}
false ---->两个对象不是一个,发了两个sql,说明缓存没有起作用

可以看见两次同样的sql,却都进库进行了查询。说明二级缓存没开。

开启二级缓存

1
<setting name="cacheEnabled" value="true"/>
Mapper

1
2
3
4
5
6
7
<!--开启本Mapper的namespace下的二级缓存-->
<cache eviction="LRU" flushInterval="100000" size="512" readOnly="true">
</cache>
<!--
创建了一个 LRU 最少使用清除缓存,每隔 100 秒刷新,最多可以存储 512 个对象,返回的对象是只
读的。
-->
测试

1
2
3
4
5
6
7
8
[com.ender.dao.UserMapper.findUserById]-==> Preparing: select
id,username,password from user where id = ?
[com.ender.dao.UserMapper.findUserById]-==> Parameters: 1(Integer)
[com.ender.dao.UserMapper.findUserById]-<== Total: 1
User{id=1, username='ender', password='123456'}
[com.ender.dao.UserMapper]-Cache Hit Ratio [com.ender.dao.UserMapper]: 0.5
User{id=1, username='ender', password='123456'}
true ---->两个对象一样了,就发了一个sql,说明缓存起了作用

第三方缓存——EhCache充当三级缓存


我们的三方缓存组件很对,最常用的比如ehcache,Memcached、redis等,我们以比较简单的 ehcache为例。

引入依赖


1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.1</version>
</dependency>

Mapper


1
2
3
4
<mapper namespace = "com.ender.entity.User" >
<cache type="org.mybatis.caches.ehcache.EhcacheCache" eviction="LRU"
flushInterval="10000" size="1024" readOnly="true"/>
</mapper>

ehcache配置


添加ehcache.xml文件,ehcache配置文件,具体配置自行百度

点击查看配置文件
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
40
41
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<!--
diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数
解释如下:
user.home – 用户主目录
user.dir – 用户当前工作目录
java.io.tmpdir – 默认临时文件路径
-->
<diskStore path="./tmpdir/Tmp_EhCache"/>
<defaultCache
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="259200"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
<!--
name:缓存名称。
maxElementsInMemory:缓存最大个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当
eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时
间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间
无穷大。
overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象
写到磁盘中。
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是
30MB。每个Cache都应该有自己的一个缓冲区。
maxElementsOnDisk:硬盘最大缓存个数。
diskPersistent:是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根
据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是
LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
-->

测试


1
2
3
4
5
6
7
8
[com.ender.dao.UserMapper.findUserById]-==> Preparing: select
id,username,password from user where id = ?
[com.ender.dao.UserMapper.findUserById]-==> Parameters: 1(Integer)
[com.ender.dao.UserMapper.findUserById]-<== Total: 1
User{id=1, username='ender', password='123456'}
[com.ender.dao.UserMapper]-Cache Hit Ratio [com.ender.dao.UserMapper]: 0.5
User{id=1, username='ender', password='123456'}
true

其实我们更加常见的是使用第三方的缓存进行存储,并且自由控制

1
2
3
4
5
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.3</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
final CacheManager cacheManager = new
CacheManager(this.getClass().getClassLoader().getResourceAsStream("ehcache.x
ml"));
// create the cache called "hello-world"
String[] cacheNames = cacheManager.getCacheNames();
for (String cacheName : cacheNames) {
System.out.println(cacheName);
}
Cache userDao = cacheManager.getCache("userDao");
Element element = new Element("testFindUserById_1",new User(1,"q","d"));
userDao.put(element);
Element element1 = userDao.get("testFindUserById_1");
User user = (User)element1.getObjectValue();
System.out.println(user);

参考笔记

评论