前言 由于自己要写一个开源项目,在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-behind
或JPA
的许多其他特性。但是它提供了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
项目,引入lombok
、web
、模块和mysql
驱动,并指定application.yml
数据源信息。这里不进行讲解了,不会的同学可以百度。
添加依赖 添加 spring-boot-starter-data-jdbc
和 mybatis-spring-boot-starter
依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependencys > ... <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-jdbc</artifactId > </dependency > <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 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;@ReadingConverter public class UserStatusReadingConverter implements Converter <String , UserStatus > { @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;@WritingConverter public class UserStatusWritingConverter implements Converter <UserStatus , String > { @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;@Data @Table("user") public class User implements Serializable { @Id @Column(value = "id") private Long id; @Column(value = "username") private String username; @Column(value = "password") private String password; @Column(value = "sex") private String sex; @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;@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;@RestController @RequestMapping(value = "/user") public class UserResource { public UserResource (UserRepository userRepository) { this .userRepository = userRepository; } @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 @Configuration @EnableJdbcAuditing @EnableJdbcRepositories(basePackages = "cn.smallbun.**.repository") public class RepositoryConfiguration extends AbstractJdbcConfiguration { @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 public interface UserRepositoryExtension { 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;@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;@Repository public class UserRepositoryExtensionImpl implements UserRepositoryExtension { @Override public List<User> list () { return sqlSession.selectList(UserRepositoryExtension.class.getName() + ".list" ); } 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;@RestController @RequestMapping(value = "/user") public class UserResource { public UserResource (UserRepository userRepository) { this .userRepository = userRepository; } @GetMapping(value = "/list") public ResponseEntity<List<User>> list() { List<User> jdbcLists = (List<User>) userRepository.findAll(); 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