前言
Spring 为了支持以统一的方式访问不同类型的数据库,提供了一个 Spring Data 框架,这个框架根据不同的数据库访问技术划分了不同的模块。上篇 《Spring 加强版 ORM 框架 Spring Data 入门》 介绍了不同模块遵循的通用规范,这篇我们来介绍下基于 JDBC 技术实现的 spring-data-jdbc 模块。
一、入门
基本的概念这里就不多说了,如果你在本篇遇到不明白的地方可以移步上一篇文章查看相关内容。
Spring Boot 内置了对 spring-data-jdbc 的支持,我们先通过一个 Spring Boot 项目了解 spring-data-jdbc 框架。首先引入相关 starter。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> <version>2.3.7.RELEASE</version> </dependency>
当然了,必要的数据库驱动也是不可缺少的。
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.27</version> </dependency>
再来配置一个数据源。
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test spring.datasource.username=root spring.datasource.password=12345678 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.type=com.zaxxer.hikari.HikariDataSource
这样必要的配置就搞定了,Spring Boot 会自动开启 spring-data-jdbc 的一些特性。
看下我们这里要操作的数据库表。
create table user ( id bigint unsigned auto_increment comment '主键' primary key, username varchar(20) null comment '用户名', password varchar(20) null comment '密码', version int unsigned null comment '版本号', create_by varchar(20) null comment '创建人', create_time datetime null comment '创建时间', update_by varchar(20) null comment '修改人', update_time datetime null comment '修改时间' )
每个数据库表都映射到 Java 中的一个类,这里 User 类定义如下。
@Data public class User { @Id private Long id; private String username; private String password; private Integer version; private String createBy; private Date createTime; private String updateBy; private Date updateTime; }
Java 类遵循驼峰命名规范,数据库表遵循下划线命名规范,这样 Spring Data 会自动将两者映射。唯一要注意的是 @Id 注解是必须的,这个注解表示数据库表的主键。
Spring Data 中使用 Repository 操作 Domain,我们还需要定义一个 Repository。
public interface UserRepository extends PagingAndSortingRepository<User,Long> { }
再来个测试用例。
@RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class) public class SpringDataJdbcTest { @Autowired private UserRepository userRepository; @Test public void testRepository() { User user = new User(); user.setUsername("hkp"); user.setPassword("123"); User result = userRepository.save(user); System.out.println(result); } }
执行后打印如下。
User(id=1, username=hkp, password=123, version=null, createBy=null, createTime=null, updateBy=null, updateTime=null)
数据成功插入到数据库,并返回了插入的数据。那么背后有何奥秘呢?这里简单进行总结。
Spring Boot 内置了对 Spring Data 的支持,引入 spring-boot-starter-data-jdbc、配置数据源之后,Spring Boot 进行一些自动化的配置,最重要的是会自动将 Repository 的子接口注册为 bean,方法执行时解析接口方法为具体的 SQL,使用 JdbcTemplate 操作数据库。
二、对象映射
一般情况,ORM 框架内部会实现 JDBC 操作数据库的通用流程,例如 Connection 的获取与关闭、Statement 的创建与关闭、参数设置、SQL 的执行等,而将一些不确定的部分交给用户控制,例如 SQL 定义、参数提供、结果映射。
spring-data-jdbc 将 ORM 框架做到了极致,用户可以只提供对象与数据库表的映射关系。不过 spring-data-jdbc 与 Hibernate 相比还可以灵活的提供 SQL 与参数,因此更灵活一些。
下面看下用户唯一必须要配置的映射关系。
表名与列名
类名与表名、类属性与表字段的映射关系,默认情况下使用驼峰命名到下划线命名转换关系。如果需要修改,可以使用对应的注解。
表名:使用 @Table 注解自定义表名,例如 @Table("user")。
主键:使用 @Id 注解定义主键列,这个注解是必须的。
表字段:使用 @Column 注解定义表字段,例如 @Column("username")。
支持的类型
数据库的字段类型与 Java 类的字段类型之间有一个默认的对应关系,spring-data-jdbc 默认支持的类型如下。
基本类型及其包装类型。
枚举类型,通过表中存入的名称转换为具体的枚举值。
String、Date、LocalDate、LocalDateTime、LocalTime。
Entity、Set<Entity>、List<Entity>、Map<Key,Entity>,其中 Entity 表示关联的表对应的类型。
由于 Repository 操作的是单个 Domain,spring-data-jdbc 仅支持 1-1、1-n 的映射关系。
1. 1-1 关系
1-1 的关系直接在 Domain 类中定义关联表对应的 Domain 类型的字段即可,不过关联表中需要有一个和主表名称相同的字段用来存储外键值。例如,user 表可能有一些扩展信息记录在 user_ext 表中。
create table user_ext ( id bigint unsigned auto_increment comment '主键' primary key, name varchar(20) null comment '姓名', age int null comment '年龄', user bigint null comment '外键' )
user_ext
表对应的 Domain 类型如下,注意有一个 user
字段记录 user
表的 id 值。
@Data public class UserExt { @Id private Long id; private Long user; private String name; private Integer age; }
此时需要修改 User
类如下。
@Data public class User { @Id private Long id; ... 省略其他字段 private UserExt ext; }
2. 1-n
关系
1-n
的关系可以在主表对应的 Domain 类上使用 Set
、List
、或者 Map
类型的字段表示关联表。例如用户可能有多个收获地址,使用如下的表来表示。
create table address ( id bigint unsigned auto_increment comment '主键' primary key, user_id bigint unsigned null comment '用户ID', user_key int unsigned null comment '用户地址的索引,从 0 开始', province_name varchar(20) null comment '省份名称', city_name varchar(20) null comment '城市名称', create_by varchar(20) null comment '创建人', create_time datetime null comment '创建时间', update_by varchar(20) null comment '修改人', update_time datetime null comment '更新时间' )
对应的 Domain 类型如下。
@Data public class Address { @Id private Long id; private Long userId; private Integer userKey; private String provinceName; private String cityName; private String createBy; private Date createTime; private String updateBy; private Date updateTime; }
分别用 Set
、List
、Map
类型在 User
类中表示如下。
@Data public class User { @Id private Long id; ... 省略其他字段 @MappedCollection(idColumn = "user_id") private Set<Address> addressSet; @MappedCollection(idColumn = "user_id", keyColumn = "user_key") private List<Address> addressList; @MappedCollection(idColumn = "user_id", keyColumn = "user_key") private Map<Integer, Address> addressMap; }
注意使用到了 @MappedCollection 注解,idColumn 表示外键,记录主表 ID,keyColumn 表示关联表在主表中的顺序,也就是 List 或 Map 中的索引位置,从 0 开始。
3. n-1、n-m 关系
n-1 和 n-m 的关系 Spring Data 不直接支持,需要转换为 1-1 表示。
乐观锁
spring-data-jdbc 支持乐观锁,在表示版本号的字段上加上 @Version 字段即可。
调用 save 方法的时候会根据版本号字段判断是否为新记录,如果是新记录执行 insert 操作,如果非新记录执行 update 操作并将版本号作为条件。
将 User 类型的 version 字段上加上 @Version 注解,修改代码如下。
@Data @Accessors(chain = true) public class User { @Id private Long id; ... 省略其他字段 @Version private Integer version; } @Test public void testRepository() { User user = new User(); user.setId(1L).setUsername("hkp").setPassword("123").setVersion(1); userRepository.save(user); }
将执行如下的 SQL。
UPDATE `USER` SET `USERNAME` = ?, `PASSWORD` = ?, `VERSION` = ?, `CREATE_BY` = ?, `CREATE_TIME` = ?, `UPDATE_BY` = ?, `UPDATE_TIME` = ? WHERE `USER`.`ID` = ? AND `USER`.`VERSION` = ?
新实体判断
save 方法兼具 insert 和 update 的功能,这取决于是否为新记录。
默认情况下先判断 id 的值,为 null 或 0 则为新记录,否则再判断 @Version 字段是否为 null 或 0 ,如果是则为新记录,否则为旧记录。
如果默认的规则不适用,可以让 Domain 类实现接口 Persistable 自定义判断逻辑。
@Data public class User implements Persistable { @Id private Long id; @Override public boolean isNew() { return this.id != null; } }
二、查询方法
Repository 中最重要的是查询方法,查询方法将映射为 SQL 。主要有两种方式来定义方法。
关键字
默认情况下通过方法名的特殊语法来映射 SQL,例如根据用户名查找用户可以如下定义。
public interface UserRepository extends PagingAndSortingRepository<User, Long> { User findByUsername(String username); }
find、by 作为关键字指定查找的主体和条件,如果使用 Idea 会有代码提示,也可以参考 官网 了解更多。
注解
方法名映射 SQL 需要学习特定的语法,如果觉得比较麻烦可以使用 @Query 注解指定 SQL,注解的优先级最高。
使用注解根据用户名查找用户的方法可以做如下修改。
public interface UserRepository extends PagingAndSortingRepository<User, Long> { @Query("select * from user where username = :username") User selectOne(String username); }
默认情况 spring-data-jdbc 会在 META-INF/jdbc-named-queries.properties 文件中查找 key 为 ${domainClass}.${queryMethodName} 的 value 作为 SQL,以上面的 selectOne 方法为例,可以在文件中定义如下的内容指定 SQL。
com.zzuhkp.demo.entity.User.selectOne=select * from user where username = :username
此时可以把 @Query
注解中指定的 SQL 去掉。
public interface UserRepository extends PagingAndSortingRepository<User, Long> { @Query User selectOne(String username); }
还可以使用 @Query.name
属性覆盖默认查找的 key。
public interface UserRepository extends PagingAndSortingRepository<User, Long> { @Query(name = "com.zzuhkp.demo.entity.User.selectOne") User selectOne(String username); }
另外如果默认的映射关系不满足需求,还可以指定 @Query.rowMapperClass
或者 @Query.resultSetExtractorClass
自定义结果映射。例如。
public class UserRowMapper implements RowMapper<User> { @Override public User mapRow(ResultSet resultSet, int i) throws SQLException { User user=new User(); user.setUsername(resultSet.getString("username")); user.setPassword(resultSet.getString("password")); return user; } } public interface UserRepository extends PagingAndSortingRepository<User, Long> { @Query(rowMapperClass = UserRowMapper.class) User selectOne(String username); }
利用 RowMapper 和 ResultSetExtractor 可以做一些多表 join 操作,这两个接口是 spring-jdbc 中的概念,可以参考 《Spring JdbcTemplate 快速上手》 了解更多。
@Query 注解只能定义 select 类型的 SQL,如果想要进行 insert、update、delete 操作,再加一个 @Modifying 注解就可以了,示例如下。
public interface UserRepository extends PagingAndSortingRepository<User, Long> { @Modifying @Query("delete from user where username = :username") int deleteOne(String username); }
三、生命周期事件
Repository 操作 Domain 的时候会产生一些事件,具体如下。
这些事件可以被 Spring 的事件监听器监听,利用这个特性可以在记录保存到数据库前设置操作人和操作时间。
首先我们定义一个 BaseEntity
保存所有 Domain 共有的属性。
@Data public class BaseEntity { @Id private Long id; private String createBy; private Date createTime; private String updateBy; private Date updateTime; }
然后修改 User
类继承 BaseEntity
。
@Data public class User extends BaseEntity { private String username; private String password; @Version private Integer version; }
最后监听 BeforeSaveEvent
事件就可以了。
@Component public class DomainEventListener { @EventListener public void setOperator(BeforeSaveEvent<BaseEntity> event) { BaseEntity entity = event.getEntity(); if (entity.getId() == null) { entity.setCreateBy("hkp"); entity.setCreateTime(new Date()); } entity.setUpdateBy("hkp"); entity.setUpdateTime(new Date()); } }
四、实体回调
除了生命周期中的事件,spring-data-jdbc 还支持 Domain 类实现一些回调接口,在 Repository 进行某些操作的时候也会回调这些接口方法,具体如下。
可以看到,回调与生命周期事件基本是类似的,同样可以利用回调来设置操作人。
public class BaseEntity implements BeforeSaveCallback<BaseEntity> { @Override public BaseEntity onBeforeSave(BaseEntity baseEntity, MutableAggregateChange<BaseEntity> mutableAggregateChange) { ... 省略设置操作人代码 return baseEntity; } }
五、日志、事务
spring-data-jdbc 底层依赖 JdbcTemplate,如果需要查看详细的日志,可以设置 JdbcTemplate 的日志级别。
spring-data-jdbc 支持 Spring 事务,直接在接口或方法上添加 @Transactional 注解即可。
六、审计
最后一个 spring-data-jdbc 的功能特性是审计,可以在 Domain 类上添加特定注解记录操作人。
@Data public class BaseEntity { @Id private Long id; @CreatedBy private String createBy; @CreatedDate private Date createTime; @LastModifiedBy private String updateBy; @LastModifiedDate private Date updateTime; }
对于日期来说采用当前时间即可,那操作人怎么办呢?需要注册一个 AuditorAware
类型的 bean 告诉框架。
@Component public class CustomAuditorAware implements AuditorAware<String> { @Override public Optional<String> getCurrentAuditor() { return Optional.of("test"); } }
另一个可选的方式是 Domain 类实现 Auditable
接口,这个接口提供了一些设置和获取操作人、操作时间的方法,这里就不再演示了。