前言

关于mybatis的使用其实蛮常用的,而其中的分页也是非常常用的。我见过有的公司直接将分页写成业务内容。其实吧,我觉得这才是正途,但是很多人认为其中存在大量重复内容,难以维护,难以统一控制质量。于是,我们就希望写查询的时候正常写,然后有个什么帮我们把这东西变成分好页的,也能够自动获取总条数。而这次呢,我放弃掉了之前自己写的分页常见,转而使用一个纯第三方的插件,用起来很简单。而且,之前我在springboot配置mybatis的时候,使用注解配置也总是失败,这次我参照官网进行配置,一切顺利,故,记录下来,方便后来搭建项目使用。
  这次项目的基础代码是建立在 https://blog.naturetrible.com/index.php/294/research/set-up-background-framework/nature/ 基础之上的,有不清楚的可以参见之前的博客。

依赖

很多时候我们对引入这些东西的时候,其实搞不清楚他们的依赖,导致,加的东西不敢删,添加的配置不敢去除,其实很多都是无用的。而这次,我们的依赖是我试验后相对明确的,如下:

<!--db start-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>${mybatis.startber.version}</version>
</dependency>
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.7</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.10</version>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.2.5</version>
</dependency>
<!--db end-->

其实,我们在操作mybatis的时候,使用的是spring对mybatis提供的支持库,不过我也不知道这是spring写的还是mybatis写的,总之,很简单,很好用。而分页插件是第三方的,似乎是个人写的,总是,用起来也很简单。如果还是要对分页有性能优化,控制sql生成的话,还是得自己实现哈。druid是我们使用的数据源。我们这次使用的数据库是postgresql,搭建的方法参见: https://blog.naturetrible.com/index.php/99/research/nature/

配置

由于这次的代码量相对多些,我们就按照依赖顺序给出最终结果并加以说明了。这次的配置除了上面的maven依赖配置以外,就是我们对数据源、mybatis和分页插件的具体配置了。

数据源

我们先说数据源的配置,下面先给出配置类:

package com.ntbrick.management.platform.config;

@Configuration
 @ConditionalOnClass(DruidDataSource.class)
 static class Druid extends DruidConfiguration {
     @Bean
     @ConfigurationProperties("spring.datasource.druid")
     public DruidDataSource dataSource(DataSourceProperties properties) {
         DruidDataSource druidDataSource = properties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
         DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(properties.determineUrl());
         String validationQuery = databaseDriver.getValidationQuery();
         if (validationQuery != null) {
             druidDataSource.setValidationQuery(validationQuery);
         }
         return druidDataSource;
     }
 }

这里其实只是注解了一个内部类,至于为什么这么写,其实我还没梳理明白,我只搞清楚了一些控制点。比如,@ConditionalOnClass(DruidDataSource.class),是说在扫描路径中发现了DruidDataSource.class的这个类生效。这个配置类里注册了一个bean,从spring.datasource.druid属性下面配置这个bean的属性。
  在application.yml文件中对应的配置信息为:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: org.postgresql.Driver
    url: jdbc:postgresql://192.168.137.98:5432/ntbrick_management_platform
    username: postgres
    password: 123456
    druid:
      #初始化连接大小
      initial-size: 8
      #最小空闲连接数
      min-idle: 5
      #最大连接数
      max-active: 10
      #查询超时时间
      query-timeout: 6000
      #事务查询超时时间
      transaction-query-timeout: 6000
      #关闭空闲连接超时时间
      remove-abandoned-timeout: 1800
      filters: stat,config

这里的属性我觉得我就不用详细解释了,用的时候自己看着改吧。需要注意的就是我这里的配置是操作postgresql数据库的。至此,我们已经把数据源配置成spring中的一个bean了。

mybatis

有了mybatis-spring-boot-starter,其实spring的配置也是非常简单的,只需要一个配置类即可。

package com.ntbrick.management.platform.config;

@Configuration // 该注解类似于spring配置文件
//@MapperScan(basePackages = "com.example.mpdemo.dao") // 扫描Dao文件
@EnableTransactionManagement //支持事务注解
public class MybatisConfig implements TransactionManagementConfigurer {

    @Autowired
    private DataSource dataSource;//默认配置文件中的数据源

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
        fb.setDataSource(dataSource);// 指定数据源(这个必须有,否则报错)
        return fb.getObject();
    }

    @Override
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return new DataSourceTransactionManager(dataSource);
    }

}

这里我保留了一个我没用到注释,就是指定扫描dao的位置。因为我的dao的位置并不集中,所以我就没配,就会从所有的bean中扫描。这里面还用到了之前配置的DataSource。最根本的,就是配置一个SqlSessionFactory类的bean,mybatis就可以使用了,而另一个是我为了使用事务而进行的配置。然后,mybatis就可以使用了。不过我们还是晚点介绍使用代码,接下来顺手把分页插件的配置也介绍了。

分页插件

这个分页插件相对简单,只要在application.yml中配置一些内容就可以了。
pagehelper:
  #数据库方言
  helper-dialect: postgresql
  #如果为true,页码过大的时候总是会显示最后一页数据,我们这里配置的是false,注意小于1的页码总是相当于第1页
  reasonable: false
  supportMethodsArguments: true
  params: count=countSql

实现

这里我们设计了两个简单的接口用来简单得测试mybatis和分页插件可用:

  • 根据ID获取实体
  • 获取分页后的实体列表

这次代码都放在com.ntbrick.management.platform.mybatis包内。

根据ID获取实体

我们需要在controller层添加一个接口,service层添加一个实现方法,dao层添加一个数据库操作方法。具体代码展示如下:

@RequestMapping("/mybatis/demo")
@RestController
public class DemoController extends BaseController {

    @Autowired
    private DemoService demoService;

    @RequestMapping("/get")
    public DefaultResponse getDemo(@RequestBody String demoId){
        return this.successResponse(demoService.getEntityById(Long.valueOf(demoId)));
    }

}

@Component
public class DemoService {

    @Autowired
    private DemoDao demoDao;

    /**
     * 根据id获取demo实体
     * @param id demo的id
     * @return demo的实体,如果没有找到,则返回null
     */
    public DemoEntity getEntityById(Long id){
        return demoDao.getEntityById(id);
    }
}

@Mapper
public interface DemoDao {

    @Select("SELECT\n" +
            "\tdi.demo_id AS demoId,\n" +
            "\tdi.NAME AS NAME,\n" +
            "\tdi.remark AS remark \n" +
            "FROM\n" +
            "\tdemo_info AS di\n" +
            "where di.demo_id =#{id}")
    DemoEntity getEntityById(@Param("id") Long id);
}

public class DemoEntity {

    private Long demoId;
    private String name;
    private String remark;

    public Long getDemoId() {
        return demoId;
    }

    public void setDemoId(Long demoId) {
        this.demoId = demoId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getRemark() {
        return remark;
    }

    public void setRemark(String remark) {
        this.remark = remark;
    }
}

这些代码我们一个类一个类得看:

  • DemoEntity: 这个是测试用的实体,和数据库表的结构是一样的,方便我们对数据库进行操作。
  • DemoController 为了可以进行交互,我们写了这个controller,提供一个可以带入参数的接口,返回查询出的实体。期间的操作都是通过DemoService实现的,所以这里也注解进了一个DemoService实例
  • DemoService 具体实现操作的服务,方便管理,我们将DemoService打上了Component注解,注册成了一个Bean。DemoService使用DemoDao具体实现数据库操作,DemoDao也是一个bean,我们注解了进来。
  • DemoDao 这个是mybatis操作的具体实现类了。首先,注意我们在类上打了@Mapper注解,这既标志了这个类mybatis的一个Mapper,也将这个Dao注册成了一个spring的bean。另外,看我们在getEntityById方法上面打的@Select注解,这标志着这是一个查询,与此类似的还有@Update,@Insert,@Delete注解,代表了不同的sql操作,里面的写法和mybatis在xml里的写法差不多。而如果在sql里面需要使用xml里的标签新型循环判断啥的,则可以把注解中的内容包裹在script标签中,用法就和xml中一模一样了。而我们在方法参数中的注解@Param则表明这个参数在sql语句中的变量名字。至此,我们已经可以操作数据库了。

获取分页后的实体列表

我们分别在controller、service和dao中添加了方法,如下:

DemoController:

@RequestMapping("/list")
public DefaultResponse getList(@RequestBody Map<String,Integer> params){

    PageContainer<DemoEntity> result=demoService.getEntityList(params.get("pageSize"),params.get("pageNum"));

    return this.successResponse(result);
}

DemoService:

public PageContainer<DemoEntity> getEntityList(Integer pageSize,Integer pageNum){

    PageParamDto pageParams=new PageParamDto();
    pageParams.setPageNum(pageNum);
    pageParams.setPageSize(pageSize);

    PageHelper.startPage(pageNum,pageSize); //开始分页
    List<DemoEntity> data=this.demoDao.getEntityList(); //正常查询数据
    PageInfo<DemoEntity> pageInfo=new PageInfo<>(data); //创建这个对象的时候,对象内会被内置分页的信息,主要是总条数
    PageContainer<DemoEntity> pageContainer = new PageContainer<>();
    pageContainer.setData(data);
    pageContainer.setTotalCount(pageInfo.getTotal());
    pageContainer.setPageInfo(pageParams);
    return pageContainer;
}

DemoDao:
@Select("SELECT\n" +
        "\tdi.demo_id AS demoId,\n" +
        "\tdi.NAME AS NAME,\n" +
        "\tdi.remark AS remark \n" +
        "FROM\n" +
        "\tdemo_info AS di\n"+
        " order by di.demo_id")
List<DemoEntity> getEntityList();

在上述代码中我们可以发现两个找不到的类,PageContainer和PageParamDto,这两个类是我们为了方便操作分页后的数据结构而创建的两个类。PageContainer类包含了分页后的数据以及这些数据的分页信息,PageParamDto则为请求分页时需要的参数。它们的代码如下:

public class PageContainer<T> {
    //页码,从1开始
    private Integer pageNum;

    //每页多少条
    private Integer pageSize;

    //查询数据
    private List<T> data;

    //查询数据总数
    private Long totalCount;

    public Integer getPageNum() {
        return pageNum;
    }

    public void setPageNum(Integer pageNum) {
        this.pageNum = pageNum;
    }

    public Integer getPageSize() {
        return pageSize;
    }

    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }

    public List<T> getData() {
        return data;
    }

    public void setData(List<T> data) {
        this.data = data;
    }

    public Long getTotalCount() {
        return totalCount;
    }

    public void setTotalCount(Long totalCount) {
        this.totalCount = totalCount;
    }

    /**
     * 将PageParamDto的PageNum与PageSize的值设置到PageContainer
     * @param pageParamDto
     */
    public void setPageInfo(PageParamDto pageParamDto) {
        this.setPageNum(pageParamDto.getPageNum());
        this.setPageSize(pageParamDto.getPageSize());
    }
}

public class PageParamDto {
    //页码,从1开始
    private int pageNum;
    //每页多少条
    private int pageSize;

    public int getPageNum() {
        return pageNum;
    }

    public void setPageNum(int pageNum) {
        this.pageNum = pageNum;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }
}

这里,我们挑重点看,重点,在service的那个方法里,重点的三行代码我们在下面再看下:

PageHelper.startPage(pageNum,pageSize); //开始分页
List<DemoEntity> data=this.demoDao.getEntityList(); //正常查询数据
PageInfo<DemoEntity> pageInfo=new PageInfo<>(data); //创建这个对象的时候,对象内会被内置分页的信息,主要是总条数

这里new对象的时候注入数据的具体方式以前本来说追下源码怎么回事的,结果最后也没有看明白。总之,分页的方式就是在查询数据前调用startPage,然后new一个PageInfo对象。这里不能嵌套进行分页,一个session一次只能进行一个分页操作。反正我们公司的项目目前没有觉得这个限制很大,就先这么用着吧。

总结

本篇文章代码较多,可能调理不是太清晰。自己敲代码试下就会明白。另外修改数据的方法我没写,和查询是一样的。另外,现在的配置下,事务是生效的,只需要在需要的方法上使用@Transaction注解就可以了。