将 Mybatis/MongoDB 集成到 SpringBoot 中的示例

本文最后更新于:2021年6月21日 晚上

前置要求

本文假设你已经了解或知道以下技能,尤其而且是勾选的内容。

  • Gradle
  • SpringBoot
  • Mybatis Plus
  • MongoDB
  • SpringBoot MongoDB Data
  • H2DB
  • SpringTest

场景

GitHub 项目, Blog 教程

需要同时使用 Mybatis-PlusMongoDB,所以就去了解了一下如何集成它们。

集成 Mybatis Plus

创建 SpringBoot 项目

使用 SpringIO 创建 SpringBoot 项目,初始依赖选择 web, h2 两个模块,gradle 配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
plugins {
id 'org.springframework.boot' version '2.1.3.RELEASE'
id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.rxliuli.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

注:数据库吾辈这里为了简单起见直接使用了 H2DB,真实项目中可能需要配置 MySQL 之类。为了简化项目依赖配置文件,所以使用了 Gradle 而非 Maven。

引入 Mybatis-Plus 和 MongoDB 依赖

build.gradle 中引入 mybatis-plus-boot-starter 依赖

1
2
3
dependencies {
implementation group: 'com.baomidou', name: 'mybatis-plus-boot-starter', version: '3.0.7.1'
}

添加测试数据库

src/resources 下创建两个 sql 文件 schema-h2.sqldata-h2.sql,简单的使用 H2DB 创建数据库/表并添加数据以供测试使用。

数据库结构:schema-h2.sql

1
2
3
4
5
6
7
8
9
create schema spring_boot_mybatis_plus_mongo;
use spring_boot_mybatis_plus_mongo;

create table user_info (
id bigint primary key not null,
name varchar(20) not null,
age tinyint not null,
sex bool not null
);

数据库测试数据:data-h2.sql

1
2
3
4
use spring_boot_mybatis_plus_mongo;

insert into user_info (id, name, age, sex) values (1, 'rx', 17, false);
insert into user_info (id, name, age, sex) values (2, '琉璃', 18, false);

配置 Mybatis Plus

application.yml 中添加数据源配置

1
2
3
4
5
6
7
# DataSource Config
spring:
datasource:
driver-class-name: org.h2.Driver
schema: classpath*:db/schema-h2.sql
data: classpath*:db/data-h2.sql
url: jdbc:h2:mem:test

添加一些实体/Dao/Service

用户信息实体类:com.rxliuli.example.springbootmybatisplusmongo.entity.UserInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@TableName("user_info")
public class UserInfo implements Serializable {
@TableId
private Long id;
@TableField
private String name;
@TableField
private Integer age;
@TableField
private Boolean sex;

public UserInfo() {
}

public UserInfo(Long id, String name, Integer age, Boolean sex) {
this.id = id;
this.name = name;
this.age = age;
this.sex = sex;
}

// getter()/setter()
}

用户信息 Dao:com.rxliuli.example.springbootmybatisplusmongo.dao.UserInfoDao

1
2
3
@Repository
public interface UserInfoDao extends BaseMapper<UserInfo> {
}

用户信息业务接口:com.rxliuli.example.springbootmybatisplusmongo.service.UserInfoService

1
2
public interface UserInfoService extends IService<UserInfo> {
}

用户信息业务接口实现类:com.rxliuli.example.springbootmybatisplusmongo.service.impl.UserInfoServiceImpl

1
2
3
@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoDao, UserInfo> implements UserInfoService {
}

配置 Mybatis Plus 扫描的路径

在启动类配置 Mybatis Plus,这点非常重要,以致于吾辈要单独列出,可能会出现的问题参见 踩坑 部分

1
2
3
4
5
6
7
@SpringBootApplication
@MapperScan("com.rxliuli.example.springbootmybatisplusmongo.**.dao.**")
public class SpringBootMybatisPlusMongoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisPlusMongoApplication.class, args);
}
}

测试使用 Mybatis Plus 的 UserInfoService

测试 Mybatis Plus 中 IService 接口的 list() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserInfoServiceTest {
@Autowired
private UserInfoService userInfoService;

@Test
public void list() {
final List<UserInfo> list = userInfoService.list();
assertThat(list)
.isNotEmpty();
}
}

集成 MongoDB

引入 MongoDB Boot Starter

build.gradle 中引入 spring-boot-starter-data-mongodb 依赖

1
2
3
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
}

配置 MongoDB

application.yml 中添加 MongoDB 的配置,现在 application.yaml 应该变成了下面这样

1
2
3
4
5
6
7
8
9
10
11
# DataSource Config
spring:
datasource:
driver-class-name: org.h2.Driver
schema: classpath*:db/schema-h2.sql
data: classpath*:db/data-h2.sql
url: jdbc:h2:mem:test
data:
# Integration mongodb
mongodb:
uri: mongodb://XXX:[email protected]:XXX/XXX

添加 Repository

定义一些简单操作的 Dao 接口:com.rxliuli.example.springbootmybatisplusmongo.repository.UserInfoLogRepository

1
2
3
4
5
6
7
8
9
10
@Repository
public interface UserInfoLogRepository extends MongoRepository<UserInfoLog, Long>, CustomUserInfoLogRepository {
/**
* 根据 id 查询用户日志信息
*
* @param id 查询的 id
* @return 用户日志
*/
UserInfoLog findUserInfoLogByIdEquals(Long id);
}

自定义更加复杂需求的 Dao 接口:com.rxliuli.example.springbootmybatisplusmongo.repository.CustomUserInfoLogRepository

1
2
3
4
5
6
7
8
9
public interface CustomUserInfoLogRepository {
/**
* 根据一些参数查询用户信息列表
*
* @param userInfoLog 参数对象
* @return 用户信息列表
*/
List<UserInfoLog> listByParam(UserInfoLog userInfoLog);
}

具体的实现类:com.rxliuli.example.springbootmybatisplusmongo.repository.UserInfoLogRepositoryImpl

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
/**
* 数据仓库 {@link UserInfoLogRepository} 的实现类,但请务必注意,实现类继承的是 {@link CustomUserInfoLogRepository} 接口,而非本应该继承的接口
*/
public class UserInfoLogRepositoryImpl implements CustomUserInfoLogRepository {
@Autowired
private MongoOperations mongoOperations;

@Override
public List<UserInfoLog> listByParam(UserInfoLog userInfoLog) {
final Criteria criteria = new Criteria();
if (userInfoLog.getUserId() != null) {
criteria.and("userId")
.is(userInfoLog.getUserId());
}
if (userInfoLog.getLogTime() != null) {
criteria.and("logTime")
.gte(userInfoLog.getLogTime());
}
if (userInfoLog.getOperate() != null) {
criteria.and("operate")
.regex(userInfoLog.getOperate());
}
return mongoOperations.find(new Query(criteria), UserInfoLog.class);
}
}

配置 MongoDB 扫描的路径

修改启动类,添加 @EnableMongoRepositories 注解用以配置 MongoDB 扫描的 Repository 路径

1
2
3
4
5
6
7
8
@SpringBootApplication
@MapperScan("com.rxliuli.example.springbootmybatisplusmongo.**.dao.**")
@EnableMongoRepositories("com.rxliuli.example.springbootmybatisplusmongo.**.repository.**")
public class SpringBootMybatisPlusMongoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisPlusMongoApplication.class, args);
}
}

测试使用 MongoDB 的 UserInfoLogRepository

  1. 测试 UserInfoLogRepository 中由 MongoDB Data 自动实现的 findUserInfoLogByIdEquals() 方法
  2. 测试 CustomUserInfoLogRepository 中自定义复杂的 listByParam() 方法
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
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserInfoLogRepositoryTest {
@Autowired
private UserInfoLogRepository userInfoLogRepository;

/**
* 初始数据,最开始要运行一次
*/
@Test
public void insert() {
userInfoLogRepository.insert(Lists.newArrayList(
new UserInfoLog(1L, 1L, "登录", LocalDateTime.now()),
new UserInfoLog(2L, 1L, "退出", LocalDateTime.now()),
new UserInfoLog(3L, 2L, "登录", LocalDateTime.now()),
new UserInfoLog(4L, 3L, "退出", LocalDateTime.now())
));
}

@Test
public void findUserInfoLogByIdEquals() {
final UserInfoLog result = userInfoLogRepository.findUserInfoLogByIdEquals(1L);
assertThat(result)
.isNotNull();
}

@Test
public void listByParam() {
final UserInfoLog userInfoLog = new UserInfoLog(null, 1L, "登",
LocalDateTime.parse("2019-02-22T08:22:16.000Z", DateTimeFormatter.ISO_DATE_TIME));
final List<UserInfoLog> result = userInfoLogRepository.listByParam(userInfoLog);
assertThat(result)
.isNotEmpty()
.allMatch(log ->
Objects.equals(userInfoLog.getUserId(), log.getUserId())
&& log.getOperate().contains(userInfoLog.getOperate())
&& log.getLogTime().isAfter(userInfoLog.getLogTime())
);
}
}

同时使用 Mybatis Dao 和 MongoDB Repository

在 Service 中添加方法

用户信息业务接口:com.rxliuli.example.springbootmybatisplusmongo.service.UserInfoService

1
2
3
4
5
6
7
8
public interface UserInfoService extends IService<UserInfo> {
/**
* 获取用户信息与用户日志的映射表
*
* @return 以 {@link UserInfo} -> {@link List<UserInfoLog>} 形式的 {@link Map}
*/
Map<UserInfo, List<UserInfoLog>> listUserInfoAndLogMap();
}

用户信息业务接口实现类:com.rxliuli.example.springbootmybatisplusmongo.service.impl.UserInfoServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoDao, UserInfo> implements UserInfoService {
@Autowired
private UserInfoLogRepository userInfoLogRepository;

@Override
public Map<UserInfo, List<UserInfoLog>> listUserInfoAndLogMap() {
final List<UserInfo> userInfoList = list();
final List<UserInfoLog> userInfoLogList = userInfoLogRepository.findAll();
final Map<Long, List<UserInfoLog>> map = userInfoLogList.stream().collect(Collectors.groupingBy(UserInfoLog::getUserId));
return userInfoList.stream()
.collect(Collectors.toMap(user -> user, user -> map.getOrDefault(user.getId(), Collections.emptyList())));
}
}

添加简单的 RestAPI 进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequestMapping("/api/user-info")
public class UserInfoApi {
@Autowired
private UserInfoService userInfoService;

@GetMapping("/list")
public List<UserInfo> list() {
return userInfoService.list();
}

@GetMapping("/list-user-info-and-log-map")
public Map<String, List<UserInfoLog>> listUserInfoAndLogMap() {
return userInfoService.listUserInfoAndLogMap().entrySet().stream()
.collect(Collectors.toMap(kv -> JsonUtil.toJson(kv.getKey()), Map.Entry::getValue));
}
}

测试 RestAPI

现在,我们启动项目并打开浏览器,应当可以在以下地址看到对应的 JSON 数据

  • 用户信息列表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    [
    {
    "id": 1,
    "name": "rx",
    "age": 17,
    "sex": false
    },
    {
    "id": 2,
    "name": " 琉璃 ",
    "age": 18,
    "sex": false
    }
    ]
  • 用户信息及对应日志映射表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    {
    "{\"id\":\"1\",\"name\":\"rx\",\"age\":17,\"sex\":false}": [
    {
    "id": 1,
    "userId": 1,
    "operate": " 登录 ",
    "logTime": "2019-02-22T16:22:16.099"
    },
    {
    "id": 2,
    "userId": 1,
    "operate": " 退出 ",
    "logTime": "2019-02-22T16:22:16.099"
    }
    ],
    "{\"id\":\"2\",\"name\":\"琉璃 \",\"age\":18,\"sex\":false}": [
    {
    "id": 3,
    "userId": 2,
    "operate": " 登录 ",
    "logTime": "2019-02-22T16:22:16.099"
    }
    ]
    }

踩坑

  1. Mybatis Plus 扫包范围
    使用 @MapperScan 限制 Mybatis Plus 扫描 Dao 的范围,注意不要扫到 MongoDB 的 Repository,否则会抛出异常

    1
    Caused by: org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'userInfoLogRepository' defined in null: Cannot register bean definition [Root bean: class [org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null] for bean 'userInfoLogRepository': There is already [Generic bean: class [org.mybatis.spring.mapper.MapperFactoryBean]; scope=singleton; abstract=false; lazyInit=false; autowireMode=2; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in file [D:\Text\spring-boot\spring-boot-mybatis-plus-mongo\out\production\classes\com\rxliuli\example\springbootmybatisplusmongo\repository\UserInfoLogRepository.class]] bound.

    原因是在 SpringMongoData 处理之前 Mybatis Plus 先扫描到并进行了代理,然后就会告诉你无法注册 SpringMongoData 相关的 Repository

  2. 使用 @EnableMongoRepositories 限制 SpringMongoData 扫描的范围

    既然说到限制,自然也不得不说一下 SpringMongoData 本身,如果你已经使用了 @MapperScan 扫描 Mybatis 需要处理的 Dao,那么添加与否并不重要。但是,吾辈要说但是了,但是,如果你先使用的 MongoDB,那么如果没有使用 @MapperScan 处理 Mybatis 的 Dao 的话,就会抛出以下异常,所以为了安全起见还是都定义了吧

    1
    Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userInfoServiceImpl': Unsatisfied dependency expressed through field 'baseMapper'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.rxliuli.example.springbootmybatisplusmongo.dao.UserInfoDao' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

    说的是自动注入 BaseMapper 失败,实际上是因为 Mybatis 的 Dao SpringMongoData 无法处理。

  3. 最好使用不同的后缀名区分 Mybatis MapperMongo Repository,或者放到不同的包
    也是为了避免扫描混乱,出现 Mybatis 扫描到 Mongo Repository 或是 Mongo 扫描到 Mybatis Mapper 的情况,出现上面的那两个错误。


那么,关于在 SpringBoot 中同时使用 Mybatis Plus 和 MongoDB 的搭建就到这里啦


将 Mybatis/MongoDB 集成到 SpringBoot 中的示例
https://blog.rxliuli.com/p/6427d24f405346eea9fb15e9f317babb/
作者
rxliuli
发布于
2020年4月18日
许可协议