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

简介: 前言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 类上使用 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 的时候会产生一些事件,具体如下。


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

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
安全 Java Ruby
我尝试了所有后端框架 — — 这就是为什么只有 Spring Boot 幸存下来
作者回顾后端开发历程,指出多数框架在生产环境中难堪重负。相比之下,Spring Boot凭借内置安全、稳定扩展、完善生态和企业级支持,成为构建高可用系统的首选,真正经受住了时间与规模的考验。
850 2
|
存储 人工智能 Java
AI 超级智能体全栈项目阶段四:学术分析 AI 项目 RAG 落地指南:基于 Spring AI 的本地与阿里云知识库实践
本文介绍RAG(检索增强生成)技术,结合Spring AI与本地及云知识库实现学术分析AI应用,利用阿里云Qwen-Plus模型提升回答准确性与可信度。
2723 90
AI 超级智能体全栈项目阶段四:学术分析 AI 项目 RAG 落地指南:基于 Spring AI 的本地与阿里云知识库实践
|
12月前
|
安全 前端开发 Java
《深入理解Spring》:现代Java开发的核心框架
Spring自2003年诞生以来,已成为Java企业级开发的基石,凭借IoC、AOP、声明式编程等核心特性,极大简化了开发复杂度。本系列将深入解析Spring框架核心原理及Spring Boot、Cloud、Security等生态组件,助力开发者构建高效、可扩展的应用体系。(238字)
|
12月前
|
人工智能 监控 Java
Spring AI Alibaba实践|后台定时Agent
基于Spring AI Alibaba框架,可构建自主运行的AI Agent,突破传统Chat模式限制,支持定时任务、事件响应与人工协同,实现数据采集、分析到决策的自动化闭环,提升企业智能化效率。
Spring AI Alibaba实践|后台定时Agent
|
消息中间件 缓存 Java
Spring框架优化:提高Java应用的性能与适应性
以上方法均旨在综合考虑Java Spring 应该程序设计原则, 数据库交互, 编码实践和系统架构布局等多角度因素, 旨在达到高效稳定运转目标同时也易于未来扩展.
969 8
|
XML Java 应用服务中间件
【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
SpringBoot专栏开篇第一章,讲述认识SpringBoot、Bean容器功能的讲解、自动装配原理的入门,还有其他常用的Springboot注解!如果想要了解SpringBoot,那么就进来看看吧!
932 2
|
监控 Kubernetes Cloud Native
Spring Batch 批处理框架技术详解与实践指南
本文档全面介绍 Spring Batch 批处理框架的核心架构、关键组件和实际应用场景。作为 Spring 生态系统中专门处理大规模数据批处理的框架,Spring Batch 为企业级批处理作业提供了可靠的解决方案。本文将深入探讨其作业流程、组件模型、错误处理机制、性能优化策略以及与现代云原生环境的集成方式,帮助开发者构建高效、稳定的批处理系统。
1019 1
|
SQL druid Java
Java数据库部分(MySQL+JDBC)(二、JDBC超详细学习笔记)(下)
Java数据库部分(MySQL+JDBC)(二、JDBC超详细学习笔记)
412 3
Java数据库部分(MySQL+JDBC)(二、JDBC超详细学习笔记)(下)
|
SQL Java 关系型数据库
Java数据库部分(MySQL+JDBC)(二、JDBC超详细学习笔记)(上)
Java数据库部分(MySQL+JDBC)(二、JDBC超详细学习笔记)
823 3
Java数据库部分(MySQL+JDBC)(二、JDBC超详细学习笔记)(上)
|
Java 关系型数据库 MySQL
mysql5.7 jdbc驱动
遵循上述步骤,即可在Java项目中高效地集成MySQL 5.7 JDBC驱动,实现数据库的访问与管理。
3329 1