MyBatis分页插件&逆向工程

一、分页插件介绍

MyBatis 通过提供插件机制,让我们可以根据自己的需要去增强MyBatis 的功能。需要注意的是,如果没有完全理解MyBatis 的运行原理和插件的工作方式,最好不要使用插件,因为它会改变系底层的工作逻辑,给系统带来很大的影响。

MyBatis 的插件可以在不修改原来的代码的情况下,通过拦截的方式,改变四大核心对象的行为,比如处理参数,处理SQL,处理结果。

适用场景

(1)分页功能

mybatis的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况下效率较低,不过使用mybatis插件可以改变该行为,只需要拦截StatementHandler类的prepare方法,改变要执行的SQL语句为分页语句即可;

(2)公共字段统一赋值

一般业务系统都会有创建者,创建时间,修改者,修改时间四个字段,对于这四个字段的赋值,实际上可以在DAO层统一拦截处理,可以用mybatis插件拦截Executor类的update方法,对相关参数进行统一赋值即可;

(3)性能监控

对于SQL语句执行的性能监控,可以通过拦截Executor类的update, query等方法,用日志记录每个方法执行的时间;

(4)其它

其实mybatis扩展性还是很强的,基于插件机制,基本上可以控制SQL执行的各个阶段,如执行阶段,参数处理阶段,语法构建阶段,结果集处理阶段,具体可以根据项目业务来实现对应业务逻辑。

1))我们可以定义很多的插件,那么这种所有的插件会形成一个链路,比如我们提交一个休假申请,先是项目经理审批,然后是部门经理审批,再是HR 审批,再到总经理审批,怎么实现层层的拦截?

这里我们需要用到一种设计模式——责任链模式,MyBatis 官网也有提到 https://mybatis.org/mybatis-3/zh/configuration.html#plugins

MyBatis分页插件&逆向工程_第1张图片

Executor 会拦截到CachingExcecutor 或者BaseExecutor。因为创建Executor 时是先创建CachingExcecutor,再包装拦截。从代码顺序上能看到。我们可以通过mybatis的分页插件来看看整个插件从包装拦截器链到执行拦截器链的过程。

在查看插件原理的前提上,我们需要来看看官网对于自定义插件是怎么来做的,官网上有介绍:通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。这里本人踩了一个坑,在Springboot中集成,同时引入了pagehelper-spring-boot-starter 导致RowBounds参数的值被刷掉了,也就是走到了我的拦截其中没有被设置值,这里需要注意,拦截器出了问题,可以Debug看一下Configuration配置类中拦截器链的包装情况。

2)代理和拦截是怎么实现的?

Executor 是openSession() 的时候创建的; StatementHandler 是SimpleExecutor.doQuery()创建的;里面包含了处理参数的ParameterHandler 和处理结果集的ResultSetHandler 的创建,创建之后即调用InterceptorChain.pluginAll(),返回层层代理后的对象。代理是由Plugin 类创建。在我们重写的 plugin() 方法里面可以直接调用returnPlugin.wrap(target, this);返回代理对象。

MyBatis分页插件&逆向工程_第2张图片

 

二、分页插件使用

1、PageHelper插件

(1)添加pom依赖


    com.github.pagehelper
    pagehelper
    1.2.15

(2)插件注册,在mybatis-config.xml 中注册插件


    
        
        
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
        
    

(3)调用

// 获取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
// 通过加载配置文件获取SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    // Mybatis在getMapper就会给我们创建jdk动态代理
    PageHelper.startPage(1, 5);
    List list=mapper.selectAll(); 
    PageInfo info = new PageInfo(list, 3);   
    System.out.println("当前页码:"+info.getPageNum());
    System.out.println("每页的记录数:"+info.getPageSize());
    System.out.println("总记录数:"+info.getTotal());
    System.out.println("总页码:"+info.getPages());
    System.out.println("是否第一页:"+info.isIsFirstPage());
    int[] nums = info.getNavigatepageNums();
    for (int i = 0; i < nums.length; i++) {
        System.out.println(nums[i]);
    }     
}

PageHelper 原理

对于插件机制我们上面已经介绍过了,在这里我们自然的会想到其所涉及的核心类 :PageInterceptor。拦截的是Executor 的两个query()方法,要实现分页插件的功能,肯定是要对我们写的sql进行改写,那么一定是在 intercept 方法中进行操作的,我们会发现这么一行代码:
 

String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);

调用到 AbstractHelperDialect 中的  getPageSql 方法:

public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
    // 获取sql
    String sql = boundSql.getSql();
    //获取分页参数对象
    Page page = this.getLocalPage();
    return this.getPageSql(sql, page, pageKey);
}   

这里可以看到会去调用 this.getLocalPage(),我们来看看这个方法:

public  Page getLocalPage() {
  return PageHelper.getLocalPage();
}
//线程独享
protected static final ThreadLocal LOCAL_PAGE = new ThreadLocal();
public static  Page getLocalPage() {
  return (Page)LOCAL_PAGE.get();
}

可以发现这里是调用的是PageHelper的一个本地线程变量中的一个 Page对象,从其中获取我们所设置的  PageSize 与 PageNum,那么他是怎么设置值的呢?请看:

PageHelper.startPage(1, 3);

public static  Page startPage(int pageNum, int pageSize) {
    return startPage(pageNum, pageSize, true);
}

public static  Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    Page page = new Page(pageNum, pageSize, count);
    page.setReasonable(reasonable);
    page.setPageSizeZero(pageSizeZero);
    Page oldPage = getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
     }
    //设置页数,行数信息
    setLocalPage(page);
    return page;
}

protected static void setLocalPage(Page page) {
        //设置值
        LOCAL_PAGE.set(page);
}

在我们调用 PageHelper.startPage(1, 3); 的时候,系统会调用 LOCAL_PAGE.set(page) 进行设置,从而在分页插件中可以获取到这个本地变量对象中的参数进行 SQL 的改写,由于改写有很多实现,我们这里用的Mysql的实现:

MyBatis分页插件&逆向工程_第3张图片

在这里我们会发现分页插件改写SQL的核心代码,这个代码就很清晰了,不必过多赘述:

public String getPageSql(String sql, Page page, CacheKey pageKey) {
    StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
    sqlBuilder.append(sql);
    if (page.getStartRow() == 0) {
        sqlBuilder.append(" LIMIT ");
        sqlBuilder.append(page.getPageSize());
    } else {
        sqlBuilder.append(" LIMIT ");
        sqlBuilder.append(page.getStartRow());
        sqlBuilder.append(",");
        sqlBuilder.append(page.getPageSize());
        pageKey.update(page.getStartRow());
    }

    pageKey.update(page.getPageSize());
    return sqlBuilder.toString();
}

pageHelper 就是这么一步一步的改写了我们的SQL 从而达到一个分页的效果

MyBatis分页插件&逆向工程_第4张图片

2、自定义分页插件

@Intercepts({
        @Signature(type = Executor.class,method = "query" ,args ={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ), // 需要代理的对象和方法
        @Signature(type = Executor.class,method = "query" ,args ={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class} ) // 需要代理的对象和方法
})
public class MyPageInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("简易版的分页插件:逻辑分页改成物理分页");

        // 修改sql 拼接Limit 0,10
        Object[] args = invocation.getArgs();
        // MappedStatement 对mapper映射文件里面元素的封装
        MappedStatement ms= (MappedStatement) args[0];
        // BoundSql 对sql和参数的封装
        Object parameterObject=args[1];
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        // RowBounds 封装了逻辑分页的参数 :当前页offset,一页数limit
        RowBounds rowBounds= (RowBounds) args[2];

        // 拿到原来的sql语句
        String sql = boundSql.getSql();
        String limitSql=sql+ " limit "+rowBounds.getOffset()+","+ rowBounds.getLimit();

        //将分页sql重新封装一个BoundSql 进行后续执行
        BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), limitSql, boundSql.getParameterMappings(), parameterObject);

        // 被代理的对象
        Executor executor= (Executor) invocation.getTarget();
        CacheKey cacheKey = executor.createCacheKey(ms, parameterObject, rowBounds, pageBoundSql);
        // 调用修改过后的sql继续执行查询
        return  executor.query(ms,parameterObject,rowBounds, (ResultHandler) args[3],cacheKey,pageBoundSql);
    }
}

拦截签名跟参数的顺序有严格要求,如果按照顺序找不到对应方法会抛出异常:org.apache.ibatis.exceptions.PersistenceException

3、伪分页

所谓伪分页就是查询数据库全部数据,然后再在内存中获取

List emps = mapper.queryEmp(new RowBounds(0,10));

三、逆向工程

1、引入pom依赖 


    org.mybatis.generator
    mybatis-generator-core
    1.4.0

2、编写配置文件



    

    
    
        
    
        
        
    

    
    

    
    
    
      
    


    
    
    
    
    
    
    
    

    
    

3、生成方法

(1) 通过程序

@Test
public void test01() throws Exception {
    List warnings = new ArrayList();
    boolean overwrite = true;
    File configFile = new File("generatorConfig.xml");
    ConfigurationParser cp = new ConfigurationParser(warnings);
    Configuration config = cp.parseConfiguration(configFile);
    DefaultShellCallback callback = new DefaultShellCallback(overwrite);
    MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
    myBatisGenerator.generate(null);
}

(2) 通过命令

java -jar mybatis-generator-core-1.4.0.jar -configfile ../src/main/resource/generatorConfig.xml -overwrite

4、targetRuntime改为MyBatis3

MyBatis3会多生成一份Example文件,我们在根据where条件查询时,不必再写sql,直接用Example来拼接条件

例:

@Test
public void test02() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
        // Mybatis在getMapper就会给我们创建jdk动态代理
        EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);

        // 使用Example实现动态条件语句
        EmpExample empExample=new EmpExample();
        EmpExample.Criteria criteria = empExample.createCriteria();
        criteria.andUserNameLike("%帅%")
                .andIdEqualTo(4);

        List emps = mapper.selectByExample(empExample);
        System.out.println(emps);
    }
}

 

你可能感兴趣的:(SSM,java,mybatis)