Mybatis-应用分析和最佳实践2

Mybatis应用分析和最佳实践

​ 以下是一些 MyBatis 的高级用法或者扩展方式,帮助我们更好地使用 MyBatis。

为什么要动态SQL

​ 避免因为前端传入的查询参数不同,所以导致写很多的if else,还需要非常注意SQL语句中的and,空格,逗号和转义的单引号,拼接和调试sql非常耗时。

​ Mybatis的动态SQL就解决了这个问题,其是基于OGNL表达式的。

动态标签

if

choose(when,otherwise)

trim(where,set)

一般用来去掉前缀后者或追


  ...

foreach

需要遍历集合的时候动态生成语句

批量操作

​ 我们在生产的项目中会有一些批量操作的场景,比如导入文件批量处理数据的情况(批量新增商户、批量修改商户信息),当数据量非常大,比如超过几万条的时候,在Java 代码中循环发送 SQL 到数据库执行肯定是不现实的,因为这个意味着要跟数据库创建几万次会话,即使我们使用了数据库连接池技术,对于数据库服务器来说也是不堪重负的。

​ 在 MyBatis 里面是支持批量的操作的,包括批量的插入、更新、删除。我们可以直 接传入一个 List、Set、Map 或者数组,配合动态 SQL 的标签,MyBatis 会自动帮我们 生成语法正确的 SQL 语句。

​ 比如我们来看两个例子,批量插入和批量更新。

批量插入

批量插入的语法是这样的,只要在 values 后面增加插入的值就可以了。

insert into tbl_emp (emp_id, emp_name, gender,email, d_id) values ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? )

在 Mapper 文件里面,我们使用 foreach 标签拼接 values 部分的语句:


  
    SELECT LAST_INSERT_ID()
  
  insert into tbl_emp (emp_id, emp_name, gender,email, d_id)
  values
  
    ( #{emps.empId},#{emps.empName},#{emps.gender},#{emps.email},#{emps.dId} )
  

Java 代码里面,直接传入一个 List 类型的参数。

我们来测试一下。效率要比循环发送 SQL 执行要高得多。最关键的地方就在于减少了跟数据库交互的次数,并且避免了开启和结束事务的时间消耗。

@Test
public void testBatchInsert() {
  List list = new ArrayList();
  long start = System.currentTimeMillis();
  int count = 100000;
  // max_allowed_packet 默认 4M,所以超过长度会报错
  for (int i = 0; i < count; i++) {
    String gender = i % 2 == 0 ? "M" : "F";
    Integer did = i % 2 == 0 ? 1 : 2;
    Employee emp = new Employee(null, "TestName" + i, gender, "[email protected]", did);
    list.add(emp);
  }

  employeeMapper.batchInsert(list);
  long end = System.currentTimeMillis();
  System.out.println("批量插入" + count + "条,耗时:" + (end - start) + "毫秒");
}

批量更新




    update tbl_emp set
    emp_name =
    
        when #{emps.empId} then #{emps.empName}
    
    ,gender =
    
        when #{emps.empId} then #{emps.gender}
    
    ,email =
    
        when #{emps.empId} then #{emps.email}
    
    where emp_id in
    
        #{emps.empId}
    

批量删除也是类似的

Batch Executor

​ 当然 MyBatis 的动态标签的批量操作也是存在一定的缺点的,比如数据量特别大的 时候,拼接出来的 SQL 语句过大。

​ MySQL 的服务端对于接收的数据包有大小限制,max_allowed_packet 默认是 4M,需要修改默认配置才可以解决这个问题。

Caused by: com.mysql.jdbc.PacketTooBigException: Packet for query is too large (7188967 > 4194304). You can change this value on the server by setting the max_allowed_packet' variable.

在我们的全局配置文件中,可以配置默认的 Executor 的类型。其中有一种BatchExecutor。

也可以在创建会话的时候指定执行器类型

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);

BatchExecutor 底层是对 JDBC ps.addBatch()的封装,原理是攒一批SQL以后再发

还有一个可能对你来说是新见到的参数,就是 ExecutorType。这个枚举类型定义了三个值:

  • ExecutorType.SIMPLE:这个执行器类型不做特殊的事情。它为每个语句的执行创建一个新的预处理语句。
  • ExecutorType.REUSE:这个执行器类型会复用预处理语句。
  • ExecutorType.BATCH:这个执行器会批量执行所有更新语句,如果 SELECT 在它们中间执行,必要时请把它们区分开来以保证行为的易读性。
JDBC BatchExecutor使用
public void testJdbcBatch() throws IOException {
  Connection conn = null;
  PreparedStatement ps = null;

  try {
    Long start = System.currentTimeMillis();
    // 打开连接
    conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true", "root", "123456");
    ps = conn.prepareStatement(
      "INSERT into blog values (?, ?, ?)");

    for (int i = 1000; i < 101000; i++) {
      Blog blog = new Blog();
      ps.setInt(1, i);
      ps.setString(2, String.valueOf(i)+"");
      ps.setInt(3, 1001);
      //ExecuteType=BATCH 就是对于这个ps的封装,批量插入500w的数据,用这个性能会得到很大改善
ps.addBatch(); } ps.executeBatch(); // conn.commit(); ps.close(); conn.close(); Long end = System.currentTimeMillis(); System.out.println("cost:"+(end -start ) +"ms"); } catch (SQLException se) { se.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (ps != null) ps.close(); } catch (SQLException se2) { } try { if (conn != null) conn.close(); } catch (SQLException se) { se.printStackTrace(); } } } }

三种Executor的区别

SimpleExecutor

每执行一次 update或 select,就开启一个 Statement对象, 用完立刻关闭 Statement对象。

ReuseExecutor
  • 执行 update或 select,以sql作为key查找 Statement对象, 存在就使用,不存在就创建
  • 用完后,不关闭 Statement对象,而是放置于Map内, 供下一次使用。
  • 简言之,就是重复使用 Statement对象
Batch Executor
  • 执行 update(没有 select,JDBC批处理不支持 select)

    • 将所有SQL都添加到批处理中( add Batch())
    • 等待统一执行( executebatch()),它缓存了多个 Statement对象,每个 Statement对象都是 add Batch()完毕后,等待逐一执行 execute Batch0批处理。与DBC批处理相同。
  • executeupdate()

    • 是一个语句访问一次数据库
  • executebatch()

    • 是一批语句访词一次数据库(具体一批发送多少条SQL跟服务端的 max allowed packet有关)。
  • Batchexecutor底层是对 JDBC

    • ps. add Batch()
    • ps. execute Batch()的封装。

嵌套(关联查询/N+1/延迟加载)

https://mybatis.org/mybatis-3...

​ 我们在查询业务数据的时候经常会遇到跨表关联查询的情况,比如查询员工就会关联部门(一对一),查询成绩就会关联课程(一对一),查询订单就会关联商品(一对多),等等。

我们映射结果有两个标签,一个是 resultType,一个是 resultMap。

​ resultType 是 select 标签的一个属性,适用于返回 JDK 类型(比如 Integer、String 等等)和实体类。这种情况下结果集的列和实体类的属性可以直接映射。如果返回的字

段无法直接映射,就要用 resultMap 来建立映射关系。 对于关联查询的这种情况,通常不能用 resultType 来映射。用 resultMap 映射,要么就是修改 dto(Data Transfer Object),在里面增加字段,这个会导致增加很多无关的字段。要么就是引用关联的对象,比如 Blog 里面包含了一个 Author 对象,这种情况 下就要用到关联查询(association,或者嵌套查询),MyBatis 可以帮我们自动做结果 的映射。

一对一的关联查询有两种配置方式:

嵌套结果



  
  
  
  
    
    
  



嵌套查询 N+1问题



  
  
   



​ 是分两次查询,当我们查询了员工信息之后,会再次发送一条SQL到数据库查询部门信息。

​ 我们只执行了一次查询员工信息的SQL(所谓的1),如果返回了N条记录,就会再发送N条到数据库查询部门信息(所谓的N),这就是我们说的N+1的问题,这样会白白的浪费我们的应用和数据库的性能。

懒加载

​ 如果我们使用了嵌套查询的方式,怎么解决这个问题?

​ 能不能等到使用部门信息的时候再去查询?这就是我们所说的延迟加载,或者叫懒加载

​ 在Mybatis里面可以通过开启延迟加载的开关来解决这个问题。

setting配置+代理

​ 在setting标签里面可以配置






lazyLoadingEnabled 决定了是否延迟加载。

aggressiveLazyLoading 决定了是不是对象的所有方法都会触发查询。

先来测试一下(也可以改成查询列表):

1、没有开启延迟加载的开关,会连续发送两次查询;

2、开启了延迟加载的开关,调用 blog.getAuthor()以及默认的(equals,clone,hashCode,toString)时才会发起第二次查询,其他方法并不会触发查询,比如 blog.getName();

3、如果开启了 aggressiveLazyLoading=true,其他方法也会触发查询,比如blog.getName()

问题:为什么可以做到延迟加载?

blog.getAuthor(),只是一个获取属性的方法,里面并没有连接数据库的代码,为什么会触发对数据库的查询呢?

是因为我们这个类被代理了

System.out.println(blog.getClass());

打印出来果然不对

class com.zzjson.domain.associate.BlogAndAuthor_$$_jvst70_0

这个类的名字后面有 jvst,是 JAVASSIST 的缩写

​ 当开启了延迟加载的开关,对象是怎么变成代理对象的?

DefaultResultSetHandler.createResultObject()

​ 既然是代理对象,那么必须要有一种创建代理对象的方法。我们有哪些实现动态代 理的方式?

​ 这个就是为什么 settings 里面提供了一个 ProxyFactory 属性。MyBatis 默认使用 JAVASSIST 创建代理对象。也可以改为 CGLIB,这时需要引入 CGLIB 的包。

CGLIB 和 JAVASSIST 区别是什么?

测试一下,我们把默认的 JAVASSIST 修改为 CGLIB,再打印这个对象。

分页

RowBounds

public void testSelectByRowBounds() throws IOException {
  SqlSession session = sqlSessionFactory.openSession();
  try {
    BlogMapper mapper = session.getMapper(BlogMapper.class);
    int start = 0; // offset
    int pageSize = 5; // limit
    RowBounds rb = new RowBounds(start, pageSize);
    List list = mapper.selectBlogList(rb); // 使用逻辑分页
    for(Blog b :list){
      System.out.println(b);
    }
  } finally {
    session.close();
  }
}

参数传入RowBounds

  • 是一个伪的分页,实际上会先查询所有,然后获取多少条

org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleRowValuesForSimpleResultMap

image-20210116151119131

手动limit

需要在java代码计算序号

PageHelper

https://github.com/pagehelper...

  • 利用插件

    • ThreadLocal来设置

依赖


    com.github.pagehelper
    pagehelper
    x.x.x

插件配置


    
        
        
    

使用

静态方法调用
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
PageInfo
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
List list = userMapper.selectAll();
//用PageInfo对结果进行包装
PageInfo page = new PageInfo(list);
参数方式

    
    
        
        
        
    
List selectByPageNumSize(
        @Param("user") User user,
        @Param("pageNumKey") int pageNum, 
        @Param("pageSizeKey") int pageSize);

MybatisGenerator

https://github.com/mybatis/ge...

​ 我们在项目中使用 MyBaits 的时候,针对需要操作的一张表,需要创建实体类、 Mapper 映射器、Mapper 接口,里面又有很多的字段和方法的配置,这部分的工作是 非常繁琐的。而大部分时候我们对于表的操作是相同的,比如根据主键查询、根据 Map 查询、单条插入、批量插入、根据主键删除等等等等。当我们的表很多的时候,意味着 有大量的重复工作。所以有没有一种办法,可以根据我们的表,自动生成实体类、Mapper 映射器、Mapper 接口,里面包含了我们需要用到的这些基本方法和 SQL 呢?

​ MyBatis 也提供了一个这样的东西,叫做 MyBatis Generator,简称 MBG。我们只需要修改一个配置文件,使用相关的 jar 包命令或者 Java 代码就可以帮助我们生成实体类、映射器和接口文件。不知道用 MyBatis 的同学有没有跟当年的我一样,还是实体类的一个一个字段,接口的一个一个方法,映射器的一条一条 SQL 去写的。

​ MBG 的配置文件里面有一个 Example 的开关,这个东西用来构造复杂的筛选条件的,换句话说就是根据我们的代码去生成 where 条件

​ 原理:在实体类中包含了两个有继承关系的 Criteria,用其中自动生成的方法来构建查询条件。把这个包含了 Criteria 的实体类作为参数传到查询参数中,在解析 Mapper映射器的时候会转换成 SQL 条件。

(mybatis-standalone 工程:

com.zzjson.domain.BlogExample

com.zzjson.BlogExampleTest)

BlogExample 里面包含了一个两个 Criteria:

Mybatis-应用分析和最佳实践2_第1张图片

实例:查询 bid=1 的 Blog,通过创建一个 Criteria 去构建查询条件:

BlogMapper mapper = session.getMapper(BlogMapper.class);
BlogExample example = new BlogExample();
BlogExample.Criteria criteria = example.createCriteria();
criteria.andBidEqualTo(1);
List list = mapper.selectByExample(example);

生成的语句

select 'true' as QUERYID, bid, name, author_id from blog WHERE ( bid = ? )

翻页

​ 在写存储过程的年代,翻页也是一件很难调试的事情,我们要实现数据不多不少准确地返回,需要大量的调试和修改。但是如果自己手写过分页,就能清楚分页的原理。

​ 在我们查询数据库的操作中,有两种翻页方式,一种是逻辑翻页(假分页),一种是物理翻页(真分页)。逻辑翻页的原理是把所有数据查出来,在内存中删选数据。 物理翻页是真正的翻页,比如 MySQL 使用 limit 语句,Oracle 使用 rownum 语句,SQLServer 使用 top 语句。

逻辑翻页

MyBatis 里面有一个逻辑分页对象 RowBounds,里面主要有两个属性,offset 和limit(从第几条开始,查询多少条)。

我们可以在 Mapper 接口的方法上加上这个参数,不需要修改 xml 里面的 SQL 语句。

public List selectBlogList(RowBounds rowBounds);

使用:mybatis-standalone- MyBatisTest-testSelectByRowBounds()

int start = 10; // offset,从第几行开始查询 
int pageSize = 5; // limit,查询多少条 
RowBounds rb = new RowBounds(start, pageSize); 
List list = mapper.selectBlogList(rb); for(Blog b :list){
    System.out.println(b);
}

​ 它的底层其实是对 ResultSet 的处理。它会舍弃掉前面 offset 条数据,然后再取剩下的数据的 limit 条。

// DefaultResultSetHandler.java
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
  DefaultResultContext resultContext = new DefaultResultContext();
  ResultSet resultSet = rsw.getResultSet();
  this.skipRows(resultSet, rowBounds);
  while(this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
    ResultMap discriminatedResultMap = this.resolveDiscriminatedResultMap(resultSet,
                                                                          resultMap, (String)null);
    Object rowValue = this.getRowValue(rsw, discriminatedResultMap, (String)null);
    this.storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); }
} 
 

​ 很明显,如果数据量大的话,这种翻页方式效率会很低(跟查询到内存中再使用subList(start,end)没什么区别)。所以我们要用到物理翻页。

物理翻页

物理翻页是真正的翻页,它是通过数据库支持的语句来翻页

第一种简单的办法就是传入参数(或者包装一个 page 对象),在 SQL 语句中翻页。

​ 第一个问题是我们要在 Java 代码里面去计算起止序号;第二个问题是:每个需要翻页的 Statement 都要编写 limit 语句,会造成 Mapper 映射器里面很多代码冗余。

那我们就需要一种通用的方式,不需要去修改配置的任何一条 SQL 语句,只要在我 们需要翻页的地方封装一下翻页对象就可以了。

​ 我们最常用的做法就是使用翻页的插件,这个是基于 MyBatis 的拦截器实现的,比如 PageHelper。

// pageSize 每一页几条
PageHelper.startPage(pn, 10);
List emps = employeeService.getAll(); // navigatePages 导航页码数
PageInfo page = new PageInfo(emps, 10);
return Msg.success().add("pageInfo", page);

​ PageHelper 是通过 MyBatis 的拦截器实现的,插件的具体原理我们后面再分析。简单地来说,它会根据 PageHelper 的参数,改写我们的 SQL 语句。比如 MySQL会生成 limit 语句,Oracle 会生成 rownum 语句,SQL Server 会生成 top 语句。

通用 Mapper

​ 问题:当我们的表字段发生变化的时候,我们需要修改实体类和 Mapper 文件定义的字段和方法。如果是增量维护,那么一个个文件去修改。如果是全量替换,我们还要去对比用 MBG 生成的文件。字段变动一次就要修改一次,维护起来非常麻烦。

​ 解决这个问题,我们有两种思路。

​ 第一个,因为 MyBatis 的 Mapper 是支持继承的(见https://github.com/mybatis/my... ) 。 所 以 我 们 可 以 把 我 们 的Mapper.xml 和 Mapper 接口都分成两个文件。一个是 MBG 生成的,这部分是固定不变的。然后创建 DAO 类继承生成的接口,变化的部分就在 DAO 里面维护。

mybatis-standalone 工程:

public interface BlogMapperExt extends BlogMapper { 
  public Blog selectBlogByName(String name);
}



  
  
    
    
    
  

  
  

所以以后只要修改 Ext 的文件就可以了。

这么做有一个缺点,就是文件会增多。

​ 既然针对每张表生成的基本方法都是一样的,也就是公共的方法部分代码都是一样的,我们能不能把这部分合并成一个文件,让它支持泛型呢?编写一个支持泛型的通用接口,比如叫 GPBaseMapper,把实体类作为参数传 入。这个接口里面定义了大量的增删改查的基础方法,这些方法都是支持泛型的。 自定义的 Mapper 接口继承该通用接口,例如 BlogMapper extends GPBaseMapper,自动获得对实体类的操作方法。遇到没有的方法,我们依然 可以在我们自己的 Mapper 里面编写。我们能想到的解决方案,早就有人做了这个事了,这个东西就叫做

通用 Mapper。 https://github.com/abel533/Ma...

​ 用途:主要解决单表的增删改查问题,并不适用于多表关联查询的场景。

除了配置文件变动的问题之外,通用 Mapper 还可以解决:

  1. 每个 Mapper 接口中大量的重复方法的定义;
  2. 屏蔽数据库的差异;
  3. 提供批量操作的方法;
  4. 实现分页。

通用 Mapper 和 PageHelper 作者是同一个人(刘增辉)。

使用方式:在 Spring 中使用时,引入 jar 包,替换 applicationContext.xml 中的
sqlSessionFactory 和 configure。


  

Mybatis-Plus

https://mybatis.plus/guide

MyBatis-Plus 是原生 MyBatis 的一个增强工具,可以在使用原生 MyBatis 的所有 功能的基础上,使用 plus 特有的功能。

MyBatis-Plus 的核心功能:

通用 CRUD:

​ 定义好 Mapper 接口后,只需要继承 BaseMapper 接口即可获得通用的增删改查功能,无需编写任何接口方法与配置文件。 条件构造器:通过 EntityWrapper(实体包装类),可以用于拼接 SQL语句,并且支持排序、分组查询等复杂的 SQL。 代码生成器:支持一系列的策略配置与全局配置,比 MyBatis 的代码生成更好用。
另外 MyBatis-Plus 也有分页的功能。

我的笔记仓库地址 gitee 快来给我点个Star吧

你可能感兴趣的:(spring)