SpringBoot2 | 第九篇:整合Mybatis通用Mapper和分页插件

​ 在上一篇中,我们介绍了Mybatis这款优秀的框架,顺便提及了民间大神开发的两款插件(通用Mapper、PageHelper),从此告别简单CURD代码的编写….

[TOC]

插件介绍

分页插件

在没有分页插件之前,写一个分页需要两条SQL语句,一条查询一条统计,然后才能计算出页码,这样的代码冗余而又枯燥,更重要的一点是数据库迁移,众所周知不同的数据库分页写法是不同的,而Mybatis不同于Hibernate的是它只提供动态SQL和结果集映射。值得庆幸的是,它虽然没有为分页提供良好的解决方案,但却提供了Interceptor以供开发者自己扩展,这也是这款分页插件的由来….

通用Mapper

通用 Mapper 是一个可以实现任意 MyBatis 通用方法的框架,项目提供了常规的增删改查操作以及 Example 相关的单表操作。通用 Mapper 是为了解决 MyBatis 使用中 90% 的基本操作,使用它可以很方便的进行开发,可以节省开发人员大量的时间,ssm项目一般都会使用它。

mapper-spring-boot-starter

在 starter 的逻辑中,如果你没有使用 @MapperScan 注解,你就需要在你的接口上增加 @Mapper注解,否则 MyBatis 无法判断扫描哪些接口。

这里的第一种用法没有用 @MapperScan 注解,所以你需要在所有接口上增加 @Mapper 注解。

注意:整合通用Mapper的包必须引对。tk.mybatis.spring.annotation.MapperScan

环境/版本一览:

  • 开发工具:Intellij IDEA 2018.2.2
  • springboot: 2.0.5.RELEASE
  • jdk:1.8.0_171
  • maven:3.3.9
  • pagehelper:1.2.5
  • tk.mybatis:2.0.2

1、搭建

1538720992484

2、pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 通用Mapper插件 -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<!-- 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
</dependencies>

3、application.yml

application.yml 文件中分别添加上数据库、Mybatis、通用Mapper、PageHelper的属性配置,这里只提供了常见场景的配置,更全的配置可以参考上文所述的文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
server:
port: 8080

spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
# 基本属性 allowMultiQueries:设置为true后,数据库那边才允许你批量更新。编码属性设置了存储数据到数据库才不会是乱码
url: jdbc:mysql://localhost:3306/chapter8?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=UTC&useSSL=false
username: root
password: 123456
# 下面为连接池的补充设置,应用到上面所有数据源中
hikari:
# 表示连接池的用户定义名称,主要显示在日志记录和JMX管理控制台中,以标识池和池配置。
pool-name: HikariPool
# 控制客户端(即您)等待池中连接的最大毫秒数。如果在没有连接可用的情况下超过此时间,则将抛出SQLException。最低可接受的连接超时为250毫秒。 默认值:30000(30秒)
connection-timeout: 3000
# 控制允许连接在池中空闲的最长时间。默认值:600000(10分钟)
idle-timeout: 600000
# 控制池中连接的最长生命周期。默认值:1800000(30分钟)
max-lifetime: 1800000

mybatis:
mapper-locations: classpath:mapper/*.xml # 注意:一定要对应 mapper 映射xml文件的所在路径
type-aliases-package: com.fatal.entity # 注意:对应实体类的路径
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 可以自控制台上输出 sql 语句

# pagehelper
pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql

# 通用Mapper
mapper:
# 主键自增回写方法,默认值MYSQL,详细说明请看文档
identity: MYSQL
mappers: tk.mybatis.mapper.common.BaseMapper
# 设置 insert 和 update 中,是否判断字符串类型!=''
not-empty: true
# 枚举按简单类型处理
enum-as-simple-type: true

通用Mapper

  • mapper.enum-as-simple-type: 枚举按简单类型处理,如果有枚举字段则需要加上该配置才会做映射
  • mapper.not-empty: 设置以后,会去判断 insert 和 update 中字符串类型 !=''

分页插件

  • pagehelper.reasonable: 分页合理化参数,默认值为false。当该参数设置为 true 时,pageNum<=0 时会查询第一页, pageNum>pages(超过总数时),会查询最后一页。默认false 时,直接根据参数进行查询。
  • support-methods-arguments: 支持通过 Mapper 接口参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。

4、sql

1
2
3
4
5
6
7
8
9
10
11
12
13
DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
`userId` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`phone` VARCHAR(255) NOT NULL,
PRIMARY KEY (`userId`)
) ENGINE=INNODB AUTO_INCREMENT=1003 DEFAULT CHARSET=utf8;

/*Data for the table `user` */

INSERT INTO `user`(`userId`,`userName`,`password`,`phone`) VALUES (1000,'石原里美','123456','132123123');

5、resources

在 resources 下新建mapper 文件夹,在 mapper 下新建UserMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.fatal.mapper.IUserMapper">

<sql id="BASE_TABLE">
user
</sql>

<sql id="BASE_COLUMN">
id,username,password,phone
</sql>

<!-- 自定义sql -->
<select id="selectUsers" resultType="com.fatal.entity.User">
SELECT
<include refid="BASE_COLUMN"/>
FROM
<include refid="BASE_TABLE"/>
</select>

</mapper>

6、entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.fatal.entity;

import lombok.Data;
import lombok.experimental.Accessors;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

/**
* 与jpa比较(比较易混淆的)
* 不同点:通用Mapper的@GeneratedValue不加默认自增,而JPA的@GeneratedValue不加默认需要手动添加id
* 相同点:都需要加上主键注解@Id
* @author: Fatal
* @date: 2018/10/4 0004 17:05
*/
@Data
@Accessors(chain = true)
public class User {

@Id // 使用通用Mapper必须给主键加上注解@Id
private Integer id;

private String username;

private String password;

private String phone;

}

7、mapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.fatal.mapper;

import com.fatal.entity.User;
import tk.mybatis.mapper.common.BaseMapper;

import java.util.List;

/**
* 继承 BaseMapper<T> 就可以了,是不是有点类似 JpaRepository
* User 映射接口
* @author: Fatal
* @date: 2018/10/4 0004 17:05
*/
public interface IUserMapper extends BaseMapper<User> {

List<User> selectUsers();

}

8、service

IUserService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.fatal.service;

import com.fatal.entity.User;
import com.github.pagehelper.PageInfo;

/**
* User 服务
* @author: Fatal
* @date: 2018/10/4 0004 17:09
*/
public interface IUserService {

Integer insert(User user);

PageInfo<User> pageSearch(int pageNum, int pageSize);

}

UserServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.fatal.service.impl;

import com.fatal.entity.User;
import com.fatal.mapper.IUserMapper;
import com.fatal.service.IUserService;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
* User 服务实现
* @author: Fatal
* @date: 2018/10/4 0004 17:10
*/
@Service
public class UserServiceImpl implements IUserService {

@Autowired
private IUserMapper userMapper; // 这里会报错,但并不会影响

@Override
public Integer insert(User user) {
// 进行校验...
return userMapper.insert(user);
}

/**
* 这个方法中用到了我们开头配置依赖的分页插件 pagehelper
* 很简单,只需要在service层传入参数,然后将参数传递给一个插件的一个静态方法即可
* @param pageNum 开始页数
* @param pageSize 每页显示的数据条数
* @return
*/
@Override
public PageInfo<User> pageSearch(int pageNum, int pageSize) {
// 进行校验...

// ==================== 普通写法 =====================
// 将参数传进这个方法就可以实现物理分页了,非常简单
PageHelper.startPage(pageNum,pageSize);
// 看语句像查询出整个 List,但是底层对其进行了增强,下面语句执行的时候添加了分页条件了
List<User> users = userMapper.selectUsers();
// 把查询结果给 PageInfo,它会帮我们取出当前页的数据
PageInfo result = new PageInfo(users);

// ==================== lambda写法 =====================
PageInfo<User> pageInfo = PageHelper.startPage(pageNum, pageSize)
.setOrderBy("id desc")
// .doSelectPageInfo(() -> this.userMapper.selectAll());
.doSelectPageInfo(userMapper::selectAll);
// TODO 分页 + 排序 userMapper::selectAll 这一句就是我们需要写的查询,有了这两款插件无缝切换各种数据库
return pageInfo;
}

}

9、controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.fatal.controller;

import com.fatal.entity.User;
import com.fatal.service.IUserService;
import com.github.pagehelper.PageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* User 控制器
* @author: Fatal
* @date: 2018/10/4 0004 17:12
*/
@RestController
@RequestMapping("/user")
public class UserController {

@Autowired
private IUserService userService;

/**
* 添加(通用 Mapper 中的方法)
* @param user
*/
@PostMapping("/")
public int insert(User user) {
// 进行校验...
return userService.insert(user);
}

/**
* 分页查询(使用分页插件)
* @param pageNum
* @param pageSize
*/
@GetMapping("/")
public Object pageSearch(
@RequestParam(name = "pageNum", required = false, defaultValue = "1") Integer pageNum,
@RequestParam(name = "pageSize", required = false, defaultValue = "10") Integer pageSize) {
// 当前页的详细信息
PageInfo<User> data = userService.pageSearch(pageNum, pageSize);
return data;
}

}

10、Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.fatal;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@MapperScan("com.fatal.mapper") // 使用通用`Mapper`时,MapperScan 要导`tk`的
public class Chapter9Application {

public static void main(String[] args) {
SpringApplication.run(Chapter9Application.class, args);
}
}

启动后访问即可

GET:http://localhost:8080/user/

查询成功

1538722443017

POST:http://localhost:8080/user/

1538722708928

添加成功

1538722733746

11、Tests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.fatal;

import com.fatal.entity.User;
import com.fatal.mapper.IUserMapper;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

/**
* 对mybatis的通用mapper接口进行了几个简单的测试
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter9ApplicationTests {

@Autowired
private IUserMapper userMapper;

@Test
public void insert() {
User user = new User().setUsername("micai").setPassword("123").setPhone("123");
int record = userMapper.insert(user);
Assert.assertNotEquals(0 ,record);
}

@Test
public void selectUsers() {
List<User> users = userMapper.selectUsers();
print(users);
Assert.assertNotEquals(0,users.size());
}

@Test
public void selectOne() {
User user = userMapper.selectOne(new User().setId(1000));
Assert.assertNotEquals(null ,user);
}

@Test
public void updateByPrimaryKeySelective() {
int record = userMapper.updateByPrimaryKeySelective(new User().setId(1000).setPassword("123123123"));
Assert.assertNotEquals(0 ,record);
}

/**
* 根据主键更新属性不为null的值
*/
@Test
public void updateByPrimaryKey() {
User user = userMapper.selectOne(new User().setId(1003));
user.setUsername("米琪");
int record = userMapper.updateByPrimaryKey(user);
Assert.assertNotEquals(0 ,record);
}

private void print(List list) {
list.forEach(System.out::println);
}

}

可以测试一下通用Mapper的方法是否好用

注意

  1. MapperScan的包必须使用 tk.mybatis.spring.annotation 下的
  2. 实体类的主键字段必须加上注解 @Id

参考文档

  1. Mybatis官方文档

  2. 通用Mapper文档(Gitee)

  3. 通用Mapper文档(GitHub)

  4. 分页插件文档

  5. 一起来学SpringBoot | 第八篇:通用Mapper与分页插件的集成

  6. MyBatis 通用 Mapper

总结

SpringBoot的知识已经有前辈在我们之前探索了。比较喜欢的博主有:唐亚峰 | Battcn方志朋的专栏程序猿DD纯洁的微笑。对这门技术感兴趣的可以去他们的博客逛逛。谢谢他们的分享~~

以上文章是我用来学习的Demo,都是基于 SpringBoot2.x 版本。

源码地址: https://github.com/ynfatal/springboot2-learning/tree/master/chapter9

学习 唐亚峰 前辈的经验