前言
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 的时候会产生一些事件,具体如下。
事件类型 | 发布时间 |
---|
BeforeDeleteEvent | Domain 被删除前 | AfterDeleteEvent | Domain 被删除后 | BeforeConvertEvent | Domain 转换为 SQL 前,判断是否为新值后,可以在这里手动设置 ID | BeforeSaveEvent | Domain 插入或更新前 | AfterSaveEvent | Domain 插入或更新后 | AfterLoadEvent | 从 ResultSet 中设置 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 进行某些操作的时候也会回调这些接口方法,具体如下。
EntityCallback | 发布时间 |
---|
BeforeDeleteCallback | Domain 被删除前 | AfterDeleteCallback | Domain 被删除后 | BeforeConvertCallback | Domain 转换为 SQL 前 | BeforeSaveCallback | Domain 保存前 | AfterSaveCallback | Domain 保存后 | AfterLoadCallback | ResultSet 设置 Domain 属性后 |
可以看到,回调与生命周期事件基本是类似的,同样可以利用回调来设置操作人。
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 接口,这个接口提供了一些设置和获取操作人、操作时间的方法,这里就不再演示了。
|