Spring Data 家族最近多了一个新成员:Spring Data JDBC(目前最新正式版是 1.0.9,项目主页是 https://spring.io/projects/spring-data-jdbc )。因为最近使用了此技术,所以便想写文对其介绍一二。
本文的内容主要涉及 Spring Data JDBC 的由来、基本使用、与现有技术的异同,以及实践中的经验。
Spring Data JDBC,顾名思义,是一个基于 JDBC 的数据库持久化框架。这一领域技术不少,常用的有 Hibernate、MyBatis,还有基于 Hibernate/JPA 的 Spring Data JPA,不太常用的有 JOOQ、QueryDSL 等。那为何 Spring 要另起炉灶,造个新轮子呢?在回答这个问题之前,先来简单介绍一下 Spring Data JDBC 的用法。
一、基本使用
和 Spring Data JPA 及其它 Spring Data 技术类似,Spring Data JDBC 的基本使用只需三步:1. 增加 Maven/Gradle 依赖;2. 定义实体类;3. 定义 Repository 接口。依赖配置跳过不介绍了,我们直接来看代码部分。
@Table("t_user")
class User {
@Id
private Long id;
private String username;
private String email;
private UserStatus status;
/** Getter/Setter **/
}
interface UserDao extends CrudRepository<User, Long>, UserRepositoryExtension {
@Query("select * from t_user where username = :username")
Optional<User> findByUsername(String username);
}
大体上看,Spring Data JDBC 的用法和 Spring Data JPA 类似,同样也可以零实现获得基本的增删改查功能。并且,同 Spring Data JPA 一样,insert 和 update 这两个操作都能通过同 CrudRepository#save
方法实现。选择方法是看实体中主键是否有值。如果没有值,那就是 insert,有,则是 update。
和 Spring Data JPA 不同地方在于:
- 不需要
@Entity
注解,@Table
、@Id
也是 Spring Data 提供的,而不是 JPA 的; - Spring Data JDBC 不支持直接通过方法名获得基本的查询功能,而是必须通过在
@Query
中定义 SQL 实现; - Spring Data JDBC 支持自定义扩展,这个在后面会详细介绍;
我个人觉得,不像 Spring Data JPA 和 Mongo 那样,不支持通过方法名获得基本查询功能是 Spring Data JDBC 的一项缺点,但不严重。毕竟,对于简单的查询,原生 SQL 写起来不麻烦。从项目长期发展角度看,这点工作量算不了什么。
那除去表面上的差异,Spring Data JDBC 和 Spring Data JPA 的不同之处又有哪些?
二、与 Spring Data JPA 的不同点
简单
Spring Data JPA 基于 Hibernate,而 Hibernate 是一个让人又爱又恨的技术。同原生 JDBC 相比,Hibernate 极大地简化了开发工作量;但另一方面,因为 Dirty Check、延迟加载、各种如 ManyToOne 等映射规则,又让 Hibernate 成为了一个复杂技术。而这些复杂性,平时很少直接用到,但是却增加了 Hibernate 的开发和调试难度。
Spring Data JDBC 的一个意义就在于,让开发人员享受类似于 Hibernate 所带来的便捷的同时,避免被 Hibernate 高级特性的过度复杂所困扰。
与 MyBatis 集成
Hibernate 与 MyBatis 正像是硬币的两面.与 Hibernate 相比,MyBatis 简单、易用、可靠,但是难免显得罗嗦了一些。这种罗嗦在基本功能层面显得更加明显。虽然,在大型项目中,基本功能实现层面的罗嗦并不是大问题(这也是互联网公司喜欢用 MyBatis 的原因),但更加简单自然是何乐而不为呢。
Spring Data JDBC 与 MyBatis 结合,自然是能够结合两者有点,基本功能得到了简化,复杂功能也能信手拈来。
Spring Data JDBC 与 MyBatis 整合有两种方式:
- 官方的整合方法 https://docs.spring.io/spring-data/jdbc/docs/1.1.0.RC1/reference/html/#jdbc.mybatis
- 基于自定义 Repository 实现
相较而言,我更喜欢第二种方法,因为官方整合方法只是用 MyBatis 实现了基本功能,这反而是 MyBatis 所不擅长的,而基于自定义 Repository 实现的方式更能发挥两者的优势。下面看一下简单示例:
首先定义扩展接口
interface UserRepositoryExtension {
void update(User user);
}
实现上面的接口,用 MyBatis 实现具体功能。类命名必须为接口名 + Impl。
@Component
class UserRepositoryExtensionImpl implements UserRepositoryExtension {
private final SqlSession sqlSession;
public void update(User user) {
return sqlSession.update("update", user);
}
}
原有接口扩展上面的接口
interface UserDao extends CrudRepository<User, Long>, UserRepositoryExtension {
}
这样就可以了,是不是很简单。
领域驱动设计
在 Spring Data JDBC 文档中,有一节提到了领域驱动设计 https://docs.spring.io/spring-data/jdbc/docs/1.1.0.RC1/reference/html/#jdbc.domain-driven-design 。文档中说到 Spring Data 的很多设计都是受了 DDD 的启发:
In the current implementation, entities referenced from an aggregate root are deleted and recreated by Spring Data JDBC.
具体 Spring Data JDBC 是如何实现 DDD 的?在介绍之前,先问大家一个问题,大家觉得数据库每个表都需要有一个 Repository 或 DAO 类与之对应吗?
答案是否。按照 DDD 的思想,只有 Aggregate Root(聚合根)才是持久化操作的唯一入口。举个例子,Order(订单)和 OrderItem(订单条目)都是订单域中的实体。但是,因为订单是聚合根,所以只有订单有对应的 Repository 类,而订单条目则没有。
那如何完成对订单条目表的数据操作呢?这篇文章《Spring Data JDBC, References, and Aggregates》 (https://spring.io/blog/2018/09/24/spring-data-jdbc-references-and-aggregates) 对此做了比较详细的介绍。
interface OrderRepository extends CrudRepository<PurchaseOrder, Long> {
@Query("select count(*) from order_item")
int countItems();
}
如上例所示,订单类(有 PurchaseOrder
表示)对应的 Repository 包含了对 order_item
表的操作,因为订单条目不是聚合根,没有自己的 Repository。所以,对订单条目的操作需要定义在订单的 Repository 中。恐怕也是这个思想上的不同,导致 Spring Data JDBC 很可能不会具备像 Spring Data JPA 和 Mongo 那样纯声明式的持久化功能了。
@Autowired OrderRepository repository;
@Test
public void createUpdateDeleteOrder() {
PurchaseOrder order = new PurchaseOrder();
order.addItem(4, "Captain Future Comet Lego set");
order.addItem(2, "Cute blue angler fish plush toy");
PurchaseOrder saved = repository.save(order);
assertThat(repository.count()).isEqualTo(1);
assertThat(repository.countItems()).isEqualTo(2);
repository.delete(saved);
assertThat(repository.count()).isEqualTo(0);
assertThat(repository.countItems()).isEqualTo(0);
}
而对于订单的 save 和 delete 操作,也会对订单条目进行操作。
对于其它更多的关于 Spring Data JDBC 和 DDD 的内容,欢迎大家自己看文章,也欢迎和我讨论。
三、最佳实践
接下来讨论一下我在使用 Spring Data JDBC 过程中总结的一些最佳实践。
DAO or Repository
我更倾向于讲使用了 Spring Data 技术的接口命名为 DAO,而不是 Repository。按照 DDD 的思想,Repository 是包含了领域知识的,需要保证聚合的数据一致性。而要在复杂业务中满足这一点,仅靠扩展一个接口是不可能做到的。因此,在 Spring Data 接口之上,还需要自己实现真正的 Repository。因此,Repository 这个名字要保留下来。
如何使用 save 方法
CrudRepository
提供了 save 方法,能同时实现 insert 和 update 两种功能。我的建议只把 save 当作是创建数据的工具,尽量不要在更新时使用它。原因在于在复杂的项目中,使用 save 进行数据更新,极容易造成数据被错误覆盖。因为 Spring Data JDBC 同 JPA 技术一样,都会根据实体类生成对全部字段更新的 update 语句,并且 Spring Data JDBC 目前没有内建的乐观锁机制。
何时使用 MyBatis
前面提到 Spring Data JDBC 可以和 MyBatis 结合使用。那什么时候一个功能应该用 Spring Data JDBC 实现,什么时候应该用 MyBatis 实现。我个人意见是当持久化方法的基本类型入参大于3个时,使用 MyBatis 实现(并将参数抽取为参数对象)。 因为,当入参大于3时,意味着参数列表和 SQL 语句都会比较复杂。使用 Spring Data JDBC,一是目前不支持参数对象,而是过长的 SQL 在注解中定义不易阅读。
如何使用枚举字段
我发现大部分项目的数据库设计喜欢使用 Int 类型表示注入状态、类型这样的枚举字段。虽然这样做对数据库性能有好处,但是对开发人员写代码的性能可是大有坏处。因为选项一多,没有几个人能记得住 1、2、3、4 各自代表什么业务含义。而且更有甚者,不同项目中,甚至同一个项目,意义类似的字段的取值各有不同。这简直是项目维护的黑洞,Bug 的源泉。
所以,在代码层面,一定要使用枚举类型表示数据库中的 Int 所代表的枚举类型。所以,在本文的第一个代码示例中,用户状态是用一个枚举类表示的
...
UserStatus status;
...
在 Spring Data JDBC 和 MyBatis 中,都有相应的机制解决枚举和 Int 转换的问题,但并不是开箱即用,而是需要写一些代码。因为篇幅问题,本文就不做具体介绍,算是挖个小坑,留到下一篇文章讲解。
四、总结
个人观点,Spring 就像是 Java 开源界的暴雪。“Spring 出品,必属精品。” 这么说其实不算很过分。
个人觉得,一方面,Spring 出品的项目,都是易用且功能强大的。更重要的是,Spring 项目的影响更多体现的设计和思想层面,总能引领某种风潮,这恐怕是 Spring 项目这么长时间以来,一直深受欢迎的原因。
因此,对与 Spring Data JDBC 这个项目,大家应更多关注。