Spring Data JDBC 结合 MyBatis 实践

前言

  由于自己要写一个开源项目,在ORM的技术选型上纠结不已,十分痛苦,后来决定采用Spring Data JDBC 和 原生 MyBatis 进行整合使用,双剑合璧,发挥其两者最大价值,将使用经验进行书写整理,已帮助更多开发者,文章若存在不正之处,还请各位同学帮忙指正,感谢。

什么是 Spring Data JDBC?

  Spring Data JDBC是较大的Spring Data系列的一部分,可轻松实现基于JDBC的存储库。该模块处理对基于JDBC的数据访问层的增强支持。它使构建使用数据访问技术的Spring支持的应用程序变得更加容易。

  如果用过Spring Data JPA 的同学可能都清楚,Spring Data JPA真是个让人又爱又恨的框架,爱是因为它上手简单,简洁强大,恨就是太过复杂,不够灵活,且难以控制,真正简单的事情在JPA中变得相当困难,为此Spring 推出了 Spring Data JDBC

优点

   它不像Spring Data JPA那么复杂。它不提供缓存、延迟加载、write-behindJPA的许多其他特性。但是它提供了Spring Data JPA使用的大多数特性,比如实体映射、通用Repository、@Query查询、根据方法名查询和JdbcTemplate

不足

   查询方面不够灵活,不支持动态SQL,复杂查询下,实现起来,相当复杂。

什么是 MyBatis?

  MyBatis感觉不必多言,很多同学都应该使用过,它是一款优秀的持久层框架,支持自定义SQL、存储过程以及高级映射。它免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

优点

  简单易学,便于维护管理,解除SQL与程序代码的耦合,支持编写动态SQL,查询的结果集与对象自动映射,接近JDBC,比较灵活。

不足

  对SQL语句依赖程度很高,并且属于半自动,编写SQL语句时工作量很大,尤其是字段多、关联表多时,更是如此。数据库移植比较麻烦,比如MySQL数据库编程Oracle数据库,部分的SQL语句需要调整。

整合使用

  在了解了两者优缺点以后,我们基本看出了两者优点和不足,但如果整合起来使用,将会弥补这些不足,简单地查询,持久化,我们可以交给Spring Data JDBC 完成,复杂的查询,我们可以交给MyBatis来完成,既减少了大量样板代码,又大大提高了代码灵活性。

  这里通过对用户CRUD操作来进行举例。

准备工作

创建用户表

我这里用的是MySQL

1
2
3
4
5
6
7
8
CREATE TABLE `user`  (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`sex` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

添加数据

1
INSERT INTO `spring-data-jdbc`.`user`(`id_`, `username_`, `password_`, `sex_`, `status_`) VALUES (1, '张三', 'admin', '1', 'enable');

创建项目

这里大家通过擅长的方式创建一个由Maven构建工具管理的Spring Boot项目,引入lombokweb、模块和mysql驱动,并指定application.yml数据源信息。这里不进行讲解了,不会的同学可以百度。

添加依赖

添加 spring-boot-starter-data-jdbcmybatis-spring-boot-starter 依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencys>
...
<!--data jdbc starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<!--mybatis starter-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
...
</dependencys>

代码编写

编写 Enum

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
/**
* 用户状态
*
* @author SanLi
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com
*/
public enum UserStatus {
/**
* 启用
*/
ENABLE("enable"),
/**
* 禁用
*/
DISABLE("disable");

private String code;

UserStatus(String code) {
this.code = code;
}

public static UserStatus getType(String string) {
UserStatus[] values = values();
for (UserStatus value : values) {
if (value.getCode().equals(string)) {
return value;
}
}
throw new RuntimeException("未找到编码");
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}
}

枚举转换

对于枚举,框架默认是通过枚举名称进行映射,但是我们往往会有自定义转换的场景,这里我们通过自定义转换器进行实现即可。这里用枚举进行举例,各位同学在开发中可以举一反三。

编写 UserStatusReadingConverter

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
import cn.smallbun.jdbc.enums.UserStatus;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;

/**
* UserStatusHandlerReadingConverter
*
* @author SanLi
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2020/12/5 21:39
*/
@ReadingConverter
public class UserStatusReadingConverter implements
Converter<String, UserStatus> {
/**
* Convert the source object of type {@code S} to target type {@code T}.
*
* @param source the source object to convert, which must be an instance of {@code S} (never {@code null})
* @return the converted object, which must be an instance of {@code T} (potentially {@code null})
* @throws IllegalArgumentException if the source cannot be converted to the desired target type
*/
@Override
public UserStatus convert(String source) {
return UserStatus.getType(source);
}
}

编写 UserStatusWritingConverter

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
import cn.smallbun.jdbc.enums.UserStatus;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.WritingConverter;

/**
* CipherComplexityRuleWritingConverter
*
* @author SanLi
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2020/12/5 21:37
*/
@WritingConverter
public class UserStatusWritingConverter implements
Converter<UserStatus, String> {
/**
* Convert the source object of type {@code S} to target type {@code T}.
*
* @param source the source object to convert, which must be an instance of {@code S} (never {@code null})
* @return the converted object, which must be an instance of {@code T} (potentially {@code null})
* @throws IllegalArgumentException if the source cannot be converted to the desired target type
*/
@Override
public String convert(UserStatus source) {
return source.getCode();
}
}

编写 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import cn.smallbun.jdbc.enums.UserStatus;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;

import java.io.Serializable;

/**
* User
*
* @author SanLi
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com
*/
@Data
@Table("user")
public class User implements Serializable {
/**
* id
*/
@Id
@Column(value = "id")
private Long id;
/**
* username
*/
@Column(value = "username")
private String username;
/**
* password
*/
@Column(value = "password")
private String password;
/**
* sex
*/
@Column(value = "sex")
private String sex;
/**
* status
*/
@Column(value = "status")
private UserStatus status;
}

@Table:当NamingStrategy与数据库表名称不匹配时,可以使用@Table批注自定义名称。value此批注的元素提供自定义表名称。
@Column:当NamingStrategy与数据库列名称不匹配时,可以使用@Column批注自定义名称。value此批注的元素提供自定义列名称。

编写 Repository

1
2
3
4
5
6
7
8
9
10
11
12
13
import cn.smallbun.jdbc.entity.User;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;

/**
* User Repository
*
* @author SanLi
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com
*/
@Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long>{
}

Spring Data JDBC 提供了 CrudRepository来完成通用CURD操作,还提供了PagingAndSortingRepository来简化分页访问。

编写 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
import cn.smallbun.jdbc.entity.User;
import cn.smallbun.jdbc.repository.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
* User Resource
*
* @author SanLi
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com
*/
@RestController
@RequestMapping(value = "/user")
public class UserResource {

public UserResource(UserRepository userRepository) {
this.userRepository = userRepository;
}

/**
* 查询列表
*
* @return {@link List<User>}
*/
@GetMapping(value = "/list")
public ResponseEntity<List<User>> list() {
List<User> list = (List<User>) userRepository.findAll();
return ResponseEntity.ok(list);
}

private final UserRepository userRepository;
}

编写 Config

新增RepositoryConfiguration配置类对Spring Data JDBC进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Repository Config
*
* @author SanLi
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com
*/
@Configuration
@EnableJdbcAuditing
@EnableJdbcRepositories(basePackages = "cn.smallbun.**.repository")
public class RepositoryConfiguration extends AbstractJdbcConfiguration {
/**
* 配置转换器
*
* @return {@link JdbcCustomConversions}
*/
@Override
public JdbcCustomConversions jdbcCustomConversions() {
return new JdbcCustomConversions(Arrays.asList(new UserStatusReadingConverter(), new UserStatusWritingConverter()));
}
}

AbstractJdbcConfiguration: 提供Spring Data JDBC所需的各种默认Bean
JdbcCustomConversions:接受的列表org.springframework.core.convert.converter.Converter。转换器应带有@ReadingConverter或注释,@WritingConverter以便控制其适用性,使其仅适用于读取或写入数据库。
@EnableJdbcAuditing :激活审计
@EnableJdbcRepositories :创建派生自接口的实现 Repository

  至此,我们可以看到熟悉的CRUD操作,这是基于Spring Data JDBC 来进行实现的。启动项目,打开浏览器访问 http://localhost:8080/user/list 查看返回数据。

整合 Mybatis

做完上述操作,我们发现其中并没有MyBatis身影,那如何将MyBatis整合进来呢?很简单,通过定义Repository扩展实现。

定义扩展接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* User 存储层扩展
*
* @author SanLi
* Created by qinggang.zuo@gmail.com
*/
public interface UserRepositoryExtension {
/**
* list
*
* @return {@link List}
*/
List<User> list();
}
编写类型转换器
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
import cn.smallbun.jdbc.enums.UserStatus;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import org.springframework.stereotype.Component;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
* UserStatusHandler
*
* @author SanLi
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com
*/
@Component
public class UserStatusHandler implements TypeHandler<UserStatus> {

@Override
public void setParameter(PreparedStatement preparedStatement, int i,
UserStatus origin, JdbcType jdbcType) throws SQLException {
preparedStatement.setString(i, origin.getCode());
}

@Override
public UserStatus getResult(ResultSet resultSet, String s) throws SQLException {
return UserStatus.getType(resultSet.getString(s));
}

@Override
public UserStatus getResult(ResultSet resultSet, int i) throws SQLException {
return UserStatus.getType(resultSet.getString(i));
}

@Override
public UserStatus getResult(CallableStatement callableStatement,
int i) throws SQLException {
return UserStatus.getType(callableStatement.getString(i));
}
}
编写Mapper.xml文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://www.mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.smallbun.jdbc.repository.UserRepositoryExtension">

<resultMap id="map" type="com.example.jdbc.entity.User">
<id column="id" property="id"/>
<id column="username" property="username"/>
<id column="password" property="password"/>
<id column="sex" property="sex"/>
<id column="status" property="status" typeHandler="com.example.jdbc.enums.handler.UserStatusHandler"/>
</resultMap>
<!--查询列表-->
<select id="list" resultMap="map">
select * from user
</select>
</mapper>
扫描Mapper.xml文件

application.yml中添加配置,扫描Mapper.xml文件

1
2
mybatis:
mapper-locations: classpath*:/mapper/*.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
import cn.smallbun.jdbc.entity.User;
import com.example.jdbc.repository.UserRepositoryExtension;
import org.apache.ibatis.session.SqlSession;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
* 用户存储层扩展实现
*
* @author SanLi
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2020/12/7 14:55
*/
@Repository
public class UserRepositoryExtensionImpl implements UserRepositoryExtension {

/**
* list
*
* @return {@link List}
*/
@Override
public List<User> list() {
return sqlSession.selectList(UserRepositoryExtension.class.getName() + ".list");
}

/**
* sql session template
*/
private final SqlSession sqlSession;

public UserRepositoryExtensionImpl(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
}

在实现上,我采用的是SqlSession来进行操作的, MyBatis 的主要 Java 接口就是 SqlSession。你可以通过这个接口来执行命令,获取映射器示例和管理事务。
在这里可以看出,我们不光可以使用 MyBatis 的 SqlSession 我们也可以使用 Spring JDBC 的 JdbcTemplate 来进行扩展查询实现。

更改 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
import cn.smallbun.jdbc.entity.User;
import cn.smallbun.jdbc.repository.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
* User Resource
*
* @author SanLi
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com
*/
@RestController
@RequestMapping(value = "/user")
public class UserResource {

public UserResource(UserRepository userRepository) {
this.userRepository = userRepository;
}

/**
* 查询列表
*
* @return {@link List<User>}
*/
@GetMapping(value = "/list")
public ResponseEntity<List<User>> list() {
// spring data jdbc
List<User> jdbcLists = (List<User>) userRepository.findAll();
// mybatis
List<User> mybatisLists = userRepository.list();
Random random = new Random();
int i = random.nextInt(2);
return ResponseEntity.ok(i == 0 ? jdbcLists : mybatisLists);
}

private final UserRepository userRepository;
}

  重新启动项目,打开浏览器访问 http://localhost:8080/user/list 查看返回数据和日志打印,我们可以从日志文件看出,我们采用了两种方式进行的查询。

总结

  一个项目中同时使用两个ORM框架有没有实际的意义呢?答案是肯定的。同时使用两个ORM框架,两者之间可以相互弥补自身的不足,以达到灵活性和便捷性同时兼顾,写操作少的模块,可以使用spring data jdbc快速完成相关功能的实现,对于读操作部分,简单地可以使用spring data jdbc实现,负责查询则可以利用mybatis来优化查询语句。两者之间的优势互补,能进一步的提升开发效率和系统性能,舒服。

  在ORM技术选型中,为什么一定要纠结用那个框架而纠结痛苦呢?何不取长补短、取百家所长,在各自擅长领域相结合,共同发力,流畅顺滑、愉快有爱的完成需求功能,岂不快哉?

参考资料

spring data jdbc docs
mybatis docs
mybatis-spring docs

Spring Data JDBC 结合 MyBatis 实践

http://pingfangushi.com/posts/49051/

作者

SanLi

发布于

2020-12-09

更新于

2020-12-10

许可协议