1 事物的回滚
对于我们开发过程,也会遇到报错的情况,这个时候为了保证数据的一致性我们就需要进行事务的回滚,比如我们有一个操作需要同时更新用户表和地址表,这个时候更新用户表的时候成功了,但是更新地址表的时候出现了一个特别长的字段,导致更新失败,这个时候,我们需要将数据进行回滚。
//用于在测试方法执行之前执行
@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();
}
}
2事物的隔离级别,
在事务的这一模块中经常会涉及到三个名词: 脏读,不可重复读,幻读
脏读 :
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问 这个数据,然后使用了这个数据。
不可重复读
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不 可重复读。例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果 只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。
幻读
是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主复本时,发现作者已将未编辑的新材料添加到该文档中。 如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免该问题。
事物的隔离级别:
事务隔离
这个时候我们就需要设置事务的隔离级别来解决这个问题
√: 可能出现 ×: 不会出现
脏读 | 不可重复读 | 幻读 | 说明 | |
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;
}
}
3延迟加载策略
3.1 什么是延迟加载
延迟加载:
就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载.
好处:
先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。
坏处:
因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。
3.2 需求
需求:
查询地址(Address)信息并且关联查询用户(User)信息。
如果先查询地址(Address)信息即可满足要求,当我们需要查询用户(User)信息时再查询用户(User)信息。把对用户(User)信息的按需去查询就是延迟加载。
实现多表操作时,我们使用了resultMap来实现一对一,一对多关系的操作。主要是通过 association、collection 实现一对一及一对多映射。
association、collection 具备延迟加载功能。
3.3 association实现延迟加载
3.3.1 未实现延迟加载的时候
我们之前未实现延迟加载的时候,每次操作,都会直接查询出我们需要的数据
可以看到图中包含多条sql的执行
3.3.2 开启延迟加载
找到对应设置
通过lazyLoadingEnabled、aggressiveLazyLoading可以对延迟加载进行配置
<settings>
<setting name="logImpl" value="LOG4J"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
添加了懒加载配置之后,我们在进行列表查询的过程中就不会再做大量的关联查询了,可以提升列表查询的效率,在我们用到具体字段之后才会进行关联查询。
3.3.3 aggressiveLazyLoading
属性开启之后,我们获取到变量的任意属性,就会触发懒加载,而关闭之后,我们只有触发到关联属性时,才会触发懒加载
3.3.4 配置每个关联字段的加载方式
在关联字段上也可以通过设置fetchType来指定加载方式。对于当个关联属性指定fetchType优先级是高于全局配置
<resultMap id="addressResultMap" type="Address">
<association property="user" column="user_id" javaType="User"
select="com.tledu.erp.mapper.IUserMapper.selectById" fetchType="lazy"/>
</resultMap>
3.4 collection实现懒加载
collection的懒加载实现和association基本类似
使用注解进行开发
4.1 mybatis常用注解说明
@Insert:实现新增
@Update:实现更新
@Delete:实现删除
@Select:实现查询
@Result:实现结果集封装
@Results:可以与@Result 一起使用,封装多个结果集
@ResultMap:实现引用@Results 定义的封装
@One:实现一对一结果集封装
@Many:实现一对多结果集封装
4.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);
}
4.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);
4.4 注解进行关联查询
4.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);
4.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);
5缓存
Mybatis 中缓存分为一级缓存,二级缓存
5.1 一级缓存
一级缓存是 SqlSession 级别的缓存,只要 SqlSession 没有 flush 或 close,它就存在。
一级缓存是 SqlSession 范围的缓存,当调用 SqlSession 的修改,添加,删除,commit(),close()等方法时,就会清空一级缓存。
5.2 二级缓存
二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。
5.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,都不会清空二级缓存
, 分页
一、为什么要分页
数据特别多的时候,单次请求返回大量的数据接口会非常慢。
对于数据量特别大的查询,我们都会采用分页查询
二、怎么设计分页
- 每页有多少个
- 当前是在第几页
- 数据的总数
- 数据列表
基于这些属性设计分页的实体类 @Data public class PageInfo<T> { /** * 每页有多少个 */ private int pageSize; /** * 当前是在第几页 */ private int currentPage; /** * 数据的总数 */ private int total; /** * 数据列表 */ private List<T> list; // 获取偏移量 public int getOffset() { return (this.currentPage - 1) * this.pageSize; } }
创建分页查询的方法 /** * 分页查询 * * @param user 查询条件 * @param offset 起始位置 * @param pageSize 每页容量 * @return 用户列表 */ List<User> page(@Param("user") User user, @Param("offset") int offset, @Param("pageSize") int pageSize); /** * 统计总数 * * @param user 查询条件 * @return 总数 */ int count(@Param("user") User user); <select id="page" resultType="User"> select * from t_user <where> <if test="user.nickname != null and user.nickname != ''"> and nickname like concat('%',#{user.nickname},'%') </if> <if test="user.username != null and user.username != ''"> and username = #{user.username} </if> </where> limit #{offset},#{pageSize}; </select> <select id="count" resultType="int"> select count(*) from t_user <where> <if test="user.nickname != null and user.nickname != ''"> and nickname like concat('%',#{user.nickname},'%') </if> <if test="user.username != null and user.username != ''"> and username = #{user.username} </if> </where> </select> 测试 @Test public void page(){ PageInfo<User> pageInfo = new PageInfo<User>(); pageInfo.setCurrentPage(1); pageInfo.setPageSize(10); User user = new User(); user.setNickname("尚云"); // 加上筛选条件,根据nickname 或 username进行筛选 List<User> list = userMapper.page(user,pageInfo.getOffset(),pageInfo.getPageSize()); pageInfo.setList(list); pageInfo.setTotal(userMapper.count(user)); System.out.println(pageInfo); }
四、分页插件
1. 引入依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.0</version>
</dependency>
2. 配置拦截器
在mybatis的配置文件中增加插件
<!--
plugins在配置文件中的位置必须符合要求,否则会报错,顺序如下:
properties?, settings?,
typeAliases?, typeHandlers?,
objectFactory?,objectWrapperFactory?,
plugins?,
environments?, databaseIdProvider?, mappers?
-->
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
<property name="param1" value="value1"/>
</plugin>
</plugins>
3. 配置插件
4. 使用插件
@Test
public void testList() throws IOException {
SqlSession session = MybatisUtils.openSession();
User condition = new User();
// 插件里提供的分页工具,在要查询之前,执行一下PageHelper.startPage(当前页数,每页的容量), 当使用工具时候,会导致懒加载失败
// 加了这个操作,插件就会在sql语句中拼接limit限制,并且还会统计总个数
PageHelper.startPage(1,5);
List<User> users = session.getMapper(IUserMapper.class).list(condition);
// 拿到结果之后通过PageInfo.of() 的方法,获得pageInfo
com.github.pagehelper.PageInfo<User> list = com.github.pagehelper.PageInfo.of(users);
System.out.println(users);
}