Spring 加强版 ORM 框架 spring-data-jdbc 入门与实践

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用版 2核4GB 50GB
简介: 前言Spring 为了支持以统一的方式访问不同类型的数据库,提供了一个 Spring Data 框架,这个框架根据不同的数据库访问技术划分了不同的模块。上篇 《Spring 加强版 ORM 框架 Spring Data 入门》 介绍了不同模块遵循的通用规范,这篇我们来介绍下基于 JDBC 技术实现的 spring-data-jdbc 模块。

前言



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 类上使用 SetList、或者 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;
}


分别用 SetListMap 类型在 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 的时候会产生一些事件,具体如下。


image.png


这些事件可以被 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 进行某些操作的时候也会回调这些接口方法,具体如下。


image.png


可以看到,回调与生命周期事件基本是类似的,同样可以利用回调来设置操作人。


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 接口,这个接口提供了一些设置和获取操作人、操作时间的方法,这里就不再演示了。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
3天前
|
Java 开发者 Spring
自动装配在Spring框架中的原理与实现方式
自动装配在Spring框架中的原理与实现方式
|
17小时前
|
Kubernetes Java Docker
使用Kubernetes部署Spring Boot应用的实践
使用Kubernetes部署Spring Boot应用的实践
|
17小时前
|
开发框架 运维 监控
深入理解Spring Boot中的日志框架选择
深入理解Spring Boot中的日志框架选择
|
17小时前
|
监控 Java API
使用Spring Boot构建企业级应用的实践
使用Spring Boot构建企业级应用的实践
|
3天前
|
IDE 前端开发 Java
Java中的Spring框架与企业级应用开发实践
Java中的Spring框架与企业级应用开发实践
|
5天前
|
Java 测试技术 数据安全/隐私保护
Spring Boot中的AOP编程实践
Spring Boot中的AOP编程实践
|
5天前
|
负载均衡 监控 Java
Spring Boot与微服务治理框架的集成方法
Spring Boot与微服务治理框架的集成方法
|
8月前
|
Java Spring
spring框架之AOP模块(面向切面),附带通知类型---超详细介绍
spring框架之AOP模块(面向切面),附带通知类型---超详细介绍
67 0
|
8月前
|
缓存 监控 Java
Spring框架之AOP(面向切面编程)
Spring框架之AOP(面向切面编程)
39 0
|
19天前
|
设计模式 SQL Java
Spring框架第四章(AOP概念及相关术语)
Spring框架第四章(AOP概念及相关术语)