1 mybatis中的事务和隔离级别
1.1 jdbc中的事务
在 JDBC 中我们可以通过手动方式将事务的提交改为手动方式,通过 setAutoCommit()方法就可以调整。 通过 JDK 文档,我们找到该方法如下:
那么我们的 Mybatis 框架因为是对 JDBC 的封装,所以 Mybatis 框架的事务控制方式,本身也是用 JDBC的 setAutoCommit()方法来设置事务提交方式的。
1.2 mybatis中的事务提交方式
Mybatis 中事务的提交方式,本质上就是调用 JDBC 的 setAutoCommit()来实现事务控制。 我们运行之前所写的代码:
//用于在测试方法执行之前执行
@BeforeEach
public void init()throws Exception{
// 设置为自动提交
sqlSession = MybatisUtils.openSession(true);
//获取dao的代理对象
userMapper = sqlSession.getMapper(IUserMapper.class);
}
// 在测试结束之后执行
@AfterEach
public void destroy()throws Exception{
//提交事务
//sqlSession.commit();
//释放资源
sqlSession.close();
}
@Test
public void testCommit(){
User condition = new User();
condition.setId(1);
condition.setNickname("尚云科技1112");
int i = userMapper.update(condition);
assertEquals(i,1);
}
@BeforeEach: 在每个方法之前加一下些操作
@AfterEach: 在每个方法之后加一些操作
这里我们设置了自动提交事务之后,就不需要在进行commit操作了
1.3 事务的回滚
对于我们开发过程,也会遇到报错的情况,这个时候为了保证数据的一致性我们就需要进行事务的回滚,比如我们有一个操作需要同时更新用户表和地址表,这个时候更新用户表的时候成功了,但是更新地址表的时候出现了一个特别长的字段,导致更新失败,这个时候,我们需要将数据进行回滚。
//用于在测试方法执行之前执行
@BeforeEach
public void init()throws Exception{
// 设置为自动提交
sqlSession = MybatisUtils.openSession();
//获取dao的代理对象
userMapper = sqlSession.getMapper(IUserMapper.class);
}
// 在测试结束之后执行
@AfterEach
public void destroy()throws Exception{
//提交事务
sqlSession.commit();
//释放资源
sqlSession.close();
}
@Test
public void testRollback(){
try{
User condition = new User();
condition.setId(1);
condition.setNickname("尚云科技111299");
int i = userMapper.update(condition);
assertEquals(i,1);
throw new Exception();
}catch (Exception e){
e.printStackTrace();
// 进行数据回滚操作
sqlSession.rollback();
}
}
1.4 事务的隔离级别
在我们学习数据库的时候,涉及到事务这一模块的时候经常会涉及到的三个名词就是
-
脏读
-
不可重复读
-
幻读
脏读 :
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问 这个数据,然后使用了这个数据。
不可重复读
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不 可重复读。例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果 只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。
幻读
是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主复本时,发现作者已将未编辑的新材料添加到该文档中。 如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免该问题。
事务隔离
这个时候我们就需要设置事务的隔离级别来解决这个问题
√: 可能出现 ×: 不会出现
脏读 | 不可重复读 | 幻读 | 说明 | |
Read uncommitted | √ | √ | √ | 直译就是"读未提交",意思就是即使一个更新语句没有提交,但是别 的事务可以读到这个改变.这是很不安全的。允许任务读取数据库中未提交的数据更改,也称为脏读。 |
Read committed | × | √ | √ | 直译就是"读提交",可防止脏读,意思就是语句提交以后即执行了COMMIT以后,别的事务才能读到这个改变. 只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 |
Repeatable read | × | × | √ | 直译就是"可以重复读",这是说在同一个事务里面先后执行同一个查询语句的时候,得到的结果是一样的.在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读。 |
Serializable | × | × | × | 直译就是"序列化",意思是说这个事务执行的时候不允许别的事务并发执行. 完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞 |
mybatis中也可以设置隔离级别,只不过增加了一个没有事务的属性
package org.apache.ibatis.session;
public enum TransactionIsolationLevel {
NONE(0),
READ_COMMITTED(2),
READ_UNCOMMITTED(1),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int level;
private TransactionIsolationLevel(int level) {
this.level = level;
}
public int getLevel() {
return this.level;
}
}
2 使用注解进行开发
2.1 mybatis常用注解说明
@Insert:实现新增
@Update:实现更新
@Delete:实现删除
@Select:实现查询
@Result:实现结果集封装
@Results:可以与@Result 一起使用,封装多个结果集
@ResultMap:实现引用@Results 定义的封装
@One:实现一对一结果集封装
@Many:实现一对多结果集封装
2.2 实现基本的CRUD
public interface IAddressDao {
@Insert("insert into t_address (addr, phone, postcode, user_id) VALUES (#{addr},#{phone},#{postcode},#{userId})")
int insert(Address address);
@Delete("delete from t_address where id = #{id}")
int delete(int id);
@Update("update t_address set addr = #{addr} where id = #{id}")
int update(Address address);
@Select("select * from t_address where id = #{id}")
Address selectById(int id);
}
2.3 使用Result进行映射
如何我们需要映射结果集的时候可以通过@Results注解进行映射
@Select("select * from t_address where id = #{id}")
@Results(id = "addressRes", value = {
//id = true 标志这个字段是主键
@Result(id = true, column = "id", property = "id"),
@Result(column = "addr", property = "addr"),
@Result(column = "phone", property = "mobile"),
})
Address selectById(int id);
2.4 注解进行关联查询
2.4.1 1对1
@Select("select * from t_address where id = #{id}")
@Results(id = "addressRes", value = {
//id = true 标志这个字段是主键
@Result(id = true, column = "id", property = "id"),
@Result(column = "addr", property = "addr"),
@Result(column = "phone", property = "mobile"),
@Result(column = "user_id", property = "user",
one = @One(select = "com.tledu.erp.mapper.IUserMapper.selectById", fetchType = FetchType.EAGER))
})
Address selectById(int id);
2.4.2 1对多
@Select("select * from t_user where id = #{id}")
@Results(id = "addressRes", value = {
//id = true 标志这个字段是主键
@Result(id = true, column = "id", property = "id"),
@Result(column = "id", property = "addressList",
many = @Many(select = "com.tledu.erp.mapper.IAddressMapper.listByUserId", fetchType = FetchType.EAGER))
})
User selectById(int id);
3 缓存
像大多数的持久化框架一样,Mybatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。
Mybatis 中缓存分为一级缓存,二级缓存
3.1 一级缓存
一级缓存是 SqlSession 级别的缓存,只要 SqlSession 没有 flush 或 close,它就存在。
3.1.1 测试
3.1.2 总结
一级缓存是 SqlSession 范围的缓存,当调用 SqlSession 的修改,添加,删除,commit(),close()等方法时,就会清空一级缓存。
第一次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,如果没有,从数据库查询用户信息。
得到用户信息,将用户信息存储到一级缓存中。
如果 sqlSession 去执行 commit 操作(执行插入、更新、删除),清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息(但是再查询时需要重新执行SQL去数据库查询),避免脏读。
第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存
中获取用户信息。缓存中没有,重新执行SQL去数据库查找,然后再放入缓存中
3.2 二级缓存
二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。
首先开启 mybatis 的二级缓存。
sqlSession1 去查询用户信息,查询到用户信息会将查询数据存储到二级缓存中
如果 SqlSession3 去执行相同 mapper 映射下 sql,执行 增删改的commit 提交,将会清空该 mapper 映射下的二级缓存区域的数据。
当然clearCache() 不会清空二级缓存的数据,如果希望清空二级缓存
sqlSession2 去查询与 sqlSession1 相同的用户信息,首先会去缓存中找是否存在数据,如果存在直接从缓存中取出数据
3.2.1 开启二级缓存
<setting name="cacheEnabled" value="true"/>
3.2.2 在映射文件中开启缓存支持
<mapper namespace="com.tledu.erp.mapper.IAddressMapper">
<!-- 开启缓存支持-->
<cache />
</mapper>
查询语句中需要指定useCache="true"
<select id="selectOne" resultMap="addressResultMap" useCache="true">
select *
from t_address
where id = #{id}
</select>
3.2.3 注意事项
当我们在使用二级缓存时,所缓存的类一定要实现 java.io.Serializable 接口,这种就可以使用序列化方式来保存对象。
@Data
public class Address implements Serializable {
private Integer id;
private String addr;
private String phone;
private String postcode;
private Integer userId;
/**
* 创建用户,关联用户表
*/
private User user;
}
3.2.4 总结
-
在使用二级缓存的时候,需要注意配置mybatis-config.xml中 开启二级缓存
-
<setting name="cacheEnabled" value="true"/>
-
然后再mapper映射文件中使用catch标签标注开启,并对需要换成的语句添加useCache=”true”
-
在mapper的映射文件中使用<cache />,代表当前mapper是开启二级缓存的
-
在需要二级缓存的查询上增加useCache = true,代表当前查询是需要缓存的
-
并且对应封装数据的实体类需要实现Serializable 接口
-
对待缓存的数据,实现Serialization接口,代表这个数据是可序列化
-
只有当sqlSession close之后,二级缓存才能生效
-
当执行增删改操作的时候,必须执行commit()才能持久化到数据库中,同时二级缓存清空
-
session.clearCache()无法清除二级缓存,如果需要清除二级缓存,可以通过sqlSessionFactory.getConfiguration().getCache("缓存id").clear();
-
但是当我们查询语句中,执行commit() 或者是close()关闭session,都不会清空二级缓存