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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
简介: 前言伴随着 Java 诞生与发展,目前 Java 界涌现出了五花八门的数据访问技术,有一些名词甚至达到了耳熟能详的程度,包括 JDBC、JTA、JPA、ORM、MyBatis 等等,这篇介绍的是 Spring Data 项目中的 spring-data-jpa 框架,了解其他相关技术可以查阅我前面的文章。

前言


伴随着 Java 诞生与发展,目前 Java 界涌现出了五花八门的数据访问技术,有一些名词甚至达到了耳熟能详的程度,包括 JDBC、JTA、JPA、ORM、MyBatis 等等,这篇介绍的是 Spring Data 项目中的 spring-data-jpa 框架,了解其他相关技术可以查阅我前面的文章。


由于这个框架目前在国内使用较少,本人也没有在实际项目中使用过,因此本篇更多的是带领大家对它有一个基本的认识,至于源码部分这里也不再进行分析,有感兴趣的小伙伴可以留言讨论交流。


背景与发展


JDBC 与 JTA


在 Java 中,一门技术的诞生往往是规范先行。为了在 Java 中以统一的方式访问不同的关系型数据库,sun 公司制定了 JDBC 规范,由各数据库厂商提供具体的实现。


JDBC 定义了对单个数据库的事务的使用方式,如果有多个数据库想同时加入事务,JDBC 就力不从心了,因此有时候我们还会听到 JTA 的声音,JTA 通过两阶段提交支持多个数据库加入一个事务。


ORM 框架


由于关系型数据库和面向对象编程的不同,加之 JDBC 规范的复杂性,在实际使用中会出现大量的样板式代码,包括创建连接、创建语句、查询数据库、操作结果转换为 Java 对象、关闭结果集、关闭语句、关闭连接。


为了应对使用 JDBC 的复杂性,诞生出了不少 ORM 框架,ORM 框架将这些通用的操作封装在内部,而将必须由用户定义的部分暴露出来,例如映射关系、一些复杂的 SQL 等等。


比较热门的一个 ORM 框架是 Hibernate,仅仅定义映射关系就足够了,Hibernate 将 JDBC 封装在内部,可以只使用框架提供的 API 操作数据库,一行 SQL 都不用手工书写。


另一种比较热门的 ORM 框架是 MyBatis,由于每个查询都需要单独提供映射关系,MyBatis 也被称为半自动化 ORM 框架,MyBatis 的 SQL 是手动定义的,因此相对 Hibernate 更灵活一些,能适用更复杂的场景,目前比较流行。


JPA


由于不同的 ORM 框架使用方法有所不同,为了统一 ORM 框架的使用,sun 公司又设计了一套 ORM 规范,这就是我们常听到的 JPA。JPA 的设计参考了 Hibernate,Hibernate 后来又反向实现了 JPA 规范,Hibernate 也成了目前最常用的 JPA 实现。


spring-data-jpa


Spring 框架作为 Java 事实上的标准,也对 JPA 进行了整合,最初在 spring-framework 框架中的 spring-orm 模块进行了整合,不过这里只是将 JPA 加入到 Spring 的事务管理中。


除了 spring-orm,Spring 对 JPA 整合的另一个模块是 spring-data-jpa,也就是今天的主题,关于上面提到的这些技术,可以用如下的图示来表示。


image.png

认识 spring-data-jpa


spring-data-jpa 其实只是 Spring Data 项目中的其中一个模块,Spring Data 项目旨在以相同或相似的方式操作不同的持久化实现,包括各种关系型数据库、非关系型数据库等。


其中 spring-data-commons 是其他模块依赖的公共模块,保证了不同持久化实现的使用方式统一,Spring Data 各模块通用的使用方式可参考《Spring 加强版 ORM 框架 Spring Data 入门》,这篇主要介绍 spring-data-jpa 特有的一些使用方式。


image.png


由于 spring-orm 已经实现了 JPA 与 Spring 事务的整合,spring-data-jpa 在底层直接复用了 spring-orm,只是在使用方式上又包装了一层,以适配 Spring Data 项目。如果你对 spring-orm 不了解,可以参考 《Spring 项目快速整合 JPA》,本篇同样参照了其中的示例。


快速上手


下面通过案例的形式演示如何在项目中使用 spring-data-jpa。


依赖引入


使用 spring-data-jpa 首先需要引入依赖,先来看下在 spring-framework 项目中的使用方式。


由于 Spring Data 项目各模块的发布时间有所不同,各模块以及底层依赖的 spring-framework 项目模块的版本号无法做到统一,Spring Data 官方提供了一个 bom 来维护版本号,将其添加到 maven pom 文件,然后就可以省略 Spring Data 各模块的版本号了。


<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-bom</artifactId>
            <version>2021.0.7</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>


除了这个 bom ,我们还需要添加其他的依赖,具体如下。


<!--JPA  依赖-->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
</dependency>
<!--JPA 实现-->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.6.9.Final</version>
</dependency>
<!--数据库驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.29</version>
</dependency>
<!--数据源-->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>4.0.3</version>
</dependency>


其中 spring-data-jpa 无需指定版本号,Hibernate 作为 JPA 的实现,除此之外还需要引入具体数据库的驱动,这里使用的是 MySQL,然后还需要引入获取 Connection 的数据源,这里使用的是 HikariCP。


映射定义


测试使用的数据库表如下。


create table user
(
    id          bigint unsigned auto_increment comment '主键'
        primary key,
    username    varchar(20)  not null comment '用户名',
    password    varchar(20)  not null comment '密码'
)


映射关系我们选择使用在实体类上添加注解配置。


@Getter
@Setter
@Entity(name = "User")
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
}


启用 JPA


每个 ORM 框架都有一个操作数据库的核心类,例如对于 MyBatis 来说是 SqlSession,对于 Hibernate 来说是 Session,对于 JPA 来说是 EntityManager,对于 Spring Data 来说则是 Repository。


不过 Spring Data 的 Repository 比较特殊,它是一个由用户定义的接口,用户可以提供自定义的实现,也可以由 Spring Data 具体模块创建接口的代理作为 bean,通过解析方法来查找具体实现。


为了创建 Repository 接口的实现 bean,可以通过 @EnableJpaRepository 注解来开启,示例代码如下。


@Configuration
@EnableJpaRepositories(basePackages = "com.zzuhkp.jpa",
        entityManagerFactoryRef = "entityManagerFactory",
        transactionManagerRef = "transactionManager")
@EnableTransactionManagement
public class JpaConfig {
    @Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
        dataSource.setDriverClassName(Driver.class.getName());
        dataSource.setUsername("root");
        dataSource.setPassword("12345678");
        return dataSource;
    }
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        Properties jpaProperties = new Properties();
        jpaProperties.put(AvailableSettings.SHOW_SQL, true);
        jpaProperties.put(AvailableSettings.FORMAT_SQL, true);
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setDataSource(dataSource());
        entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        entityManagerFactoryBean.setJpaProperties(jpaProperties);
        entityManagerFactoryBean.setPackagesToScan("com.zzuhkp.jpa.entity");
        return entityManagerFactoryBean;
    }
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        transactionManager.setDataSource(dataSource());
        return transactionManager;
    }
}


spring-data-jpa 依赖 spring-orm,因此有一些配置是和在 spring-orm 中使用 JPA 相同的,包括数据源、EntityManagerFactory、事务管理器。


@EnableJpaRepositories 注解的 basePackages 属性可以指定为哪些包中的 Repository 创建代理 bean。

entityManagerFactoryRef 可以指定底层依赖的 EntityManagerFactory,默认值为 entityManagerFactory。

transactionManagerRef 可以指定底层依赖的事务管理器,默认值为 transactionManager。


数据库操作


数据操作最重要的是 Repository 的定义,我们一般会定义一个继承 spring-data-jpa 模块提供的 JpaRepository 接口的 Repository 接口,这种方式可以大大简化一些常用操作方法的重复定义,这有些类似 MyBatis-Plus 框架提供的 BaseMapper。示例如下。


public interface UserRepository extends JpaRepository<User, Long> {
}


根据前面 @EnableJpaRepository 注解的配置,spring-data-jpa 会自动为这个接口提供实现并注册为 bean。


基本操作


对于单表的一些简单操作,使用 JpaRepository 接口提供的方法就可以了,JpaRepository 还继承了一些其他接口,使用非常方便,接口定义如下。


public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
}


PagingAndSortingRepository 是所有 Spring Data 模块都支持的接口,提供了基本的增删改查方法,QueryByExampleExecutor 是 spring-data-jpa 模块特有的接口,用于复杂查询,先看下 JpaRepository 提供的一些方法。


1. 添加/修改


public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    <S extends T> List<S> saveAll(Iterable<S> entities);
    <S extends T> S saveAndFlush(S entity);
    <S extends T> List<S> saveAllAndFlush(Iterable<S> entities);
}

与其他 Spring Data 模块一样,spring-data-jpa 对于添加和修改操作,使用的是相同的方法,根据是否为新记录决定进行何种操作。


默认情况先根据版本号字段和标识符字段判断是否为新实体,如果实体中的标识符字段值是手动设置的,可以选择将实体实现接口 Persistable 自定义判断逻辑。


2. 删除


JpaRepository 包含的删除方法如下。


public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    void deleteAllInBatch(Iterable<T> entities);
    void deleteAllByIdInBatch(Iterable<ID> ids);
    void deleteAllInBatch();
}


3. 查询


JpaRepository 包含的简单查询方法如下。


public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    List<T> findAll();
    List<T> findAll(Sort sort);
    List<T> findAllById(Iterable<ID> ids);
}


Example 查询


Example 能解决一些比 CrudRepository 更复杂的问题,主要用于属性匹配,需要 Repository 继承接口 QueryByExampleExecutor 才可以,示例如下。


public User login(String username, String password) {
    User user = new User();
    user.setUsername(username);
    user.setPassword(password);
    Optional<User> one = userRepository.findOne(Example.of(user));
    return one.get();
}

等价于如下 SQL:

select id,username,password from user where username = ? and password = ?


Specification 查询


spring-data-jpa 提供了对 JPA CriteriaQuery 的支持,将其封装在了 Specification 接口内部,通过 Repository 继承接口 JpaSpecificationExecutor 就可以使用了,示例如下。


public User login(String username, String password) {
    Optional<User> one = userRepository.findOne(new Specification<User>() {
        @Override
        public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
            return criteriaBuilder.equal(root.get("username"), username);
        }
    }.and(new Specification<User>() {
        @Override
        public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
            return criteriaBuilder.equal(root.get("password"), password);
        }
    }));
    return one.get();
}


上面的查询等价于如下的 SQL:

select * from user where username = ? and password = ?


可以看到,这种使用方式实在太复杂了,强烈不建议使用。


方法解析查询


方法解析策略


对于一些复杂方法的查询,主要依靠在 Repository 自定义查询方法实现的,那么 spring-data-jpa 是怎样将方法签名解析为具体的 SQL 执行的呢?


可以通过 @EnableJpaRepository 注解的 queryLookupStrategy 属性配置,各取值含义如下。


取值

含义

CREATE 从方法名中解析 SQL
USE_DECLARED_QUERY 从声明的查询中解析 SQL
CREATE_IF_NOT_FOUND 优先从声明的查询中解析 SQL,如果解析不到则使用方法名解析,这个是默认的查找策略


方法名解析 SQL


根据方法名解析 SQL 需要方法名遵循特定的结构,以用户登录查询用户信息为例,示例如下。


public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsernameAndPassword(String username, String password);
}


这将转换为如下的 SQL:

select id,username,password from user where username = ? and password = ?

如果你使用的 IDE 是 idea,还会有非常智能的代码提示。


image.png


更多查询关键字,可以参考 spring-data-jpa 官网 附录 C:存储库查询关键字 一节。


声明查询解析 SQL


1. 命名查询


spring-data-jpa 对 JPA 提供的命名查询提供了支持,可以将命名查询的注解用在实体类上。


@NamedQuery(name = "User.login", query = "select u from User u where u.username = ?1 and u.password = ?2")
public class User {
}

然后在 Repository 中将命名查询的名字作为方法名即可。

public interface UserRepository extends JpaRepository<User, Long> {
    User login(String username, String password);
}


2. @Query 查询


spring-data-jpa 还提供了一个 Query 注解,同时支持 JPQL 和 SQL,命名查询和非命名查询,将这个注解添加在 Repository 方法上即可,与上述 @NamedQuery 等价的 @Query 注解如下。


public interface UserRepository extends JpaRepository<User, Long> {
    @Query("select u from User where u.username = ?1 and u.password = ?2")
    User login(String username, String password);
}

如果想使用 SQL 查询,可以修改如下。

public interface UserRepository extends JpaRepository<User, Long> {
    @Query(value = "select * from user where username = ?1 and password = ?2",
            nativeQuery = true)
    User login(String username, String password);
}


如果想使用命名查询,可以将 JPQL 或 SQL 配置在 META-INF/jpa-named-queries.properties 文件中。


User.login = select u from User u where u.username = ?1 and u.password = ?2


然后在 @Query 注解中指定名称即可。


public interface UserRepository extends JpaRepository<User, Long> {
    @Query(name = "User.login")
    User login(String username, String password);
}


另外这个命名查询文件的位置还可以在 @EnableJpaRepository 的 namedQueriesLocation 属性中配置。


分页与排序


如果需要分页或者排序,可以将 Pageable 或 Sort 参数附加在方法参数后面,同时支持 @NamedQuery 与 @Query 注解,示例如下。


public interface UserRepository extends JpaRepository<User, Long> {
    @Query("select u from User u where u.username = ?1")
    Page<User> page(String username, Pageable pageable);
    @Query("select u from User u where u.username = ?1")
    List<User> sort(String username, Sort sort);
}


命名参数


默认情况,spring-data-jpa 采用基于位置的参数绑定,在重构代码时很容易出错,为了解决这个问题,spring-data-jpa 添加了对命名参数的支持,使用示例如下。

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("select u from User u where u.username = :user and u.password = :pwd")
    User login(@Param("user") String username, @Param("pwd") String password);
}


JPQL 或 SQL 中的参数使用 :参数名 的形式表示,然后在方法参数前使用 @Param 指定对应的 JPQL 或 SQL 参数。


修改查询


spring-data-jpa 同样支持复杂的修改 SQL,在方法上添加 @Modifying 注解即可,示例如下。

public interface UserRepository extends JpaRepository<User, Long> {
    @Modifying
    @Query("delete from User u where u.username = :user and u.password = :pwd")
    int delete(@Param("user") String username, @Param("pwd") String password);
}


事务


spring-data-jpa 默认支持 Spring 的事务,对于查询操作 readyOnly 将设置为 true,也可以在 Repository 方法上手动修改事务配置。

public interface UserRepository extends JpaRepository<User, Long> {
    @Transactional(timeout = 20)
    User findByUsernameAndPassword(String username, String password);
}


审计


审计用于记录创建或修改的人以及时间,spring-data-jpa 提供了独有的支持。

public class User {
    @CreatedDate
    private Date createTime;
    @CreatedBy
    private String createBy;
    @LastModifiedDate
    private Date updateTime;
    @LastModifiedBy
    private String updateBy;
}


在实体类上添加上面的注解即可,spring-data-jpa 可以取当前时间作为创建或修改时间,对于操作人则需要手动告诉 spring-data-jpa


@EnableJpaAuditing
public class JpaConfig {
    @Bean
    public AuditorAware<String> auditorAware() {
        return new AuditorAware<String>() {
            @Override
            public Optional<String> getCurrentAuditor() {
                return Optional.of("currentUser");
            }
        };
    }
}    


AuditorAware bean 用来指定当前操作人,@EnableJpaAuditing 注解则用于开启审计功能。


如果不想使用注解,还可以将实体类实现 Auditable 接口,这里不再赘述。


spring-boot 整合


spring-boot 对 spring-data-jpa 的支持主要在于自动化配置,添加一个 starter 即可。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

这个 starter 包含了 Hibernate、HikariCP 的依赖,Spring 会自动配置 DataSource、EntityManagerFactory、TransactionManager,以及启用 JPA Repository bean 注册与查找的功能,当然了,必须的数据库驱动还是需要用户手动提供的。


总结

spring-data-jpa 对 JPA 进行了封装与简化,如果需要使用 JPA,建议直接引入 spring-boot-starter-data-jpa。


目录
相关文章
|
26天前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
23天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,帮助开发者提高开发效率和应用的可维护性。
40 2
|
22天前
|
消息中间件 NoSQL Java
springboot整合常用中间件框架案例
该项目是Spring Boot集成整合案例,涵盖多种中间件的使用示例,每个案例项目使用最小依赖,便于直接应用到自己的项目中。包括MyBatis、Redis、MongoDB、MQ、ES等的整合示例。
80 1
|
30天前
|
Java 数据库连接 开发者
Spring 框架:Java 开发者的春天
【10月更文挑战第27天】Spring 框架由 Rod Johnson 在 2002 年创建,旨在解决 Java 企业级开发中的复杂性问题。它通过控制反转(IOC)和面向切面的编程(AOP)等核心机制,提供了轻量级的容器和丰富的功能,支持 Web 开发、数据访问等领域,显著提高了开发效率和应用的可维护性。Spring 拥有强大的社区支持和丰富的生态系统,是 Java 开发不可或缺的工具。
|
28天前
|
数据采集 Java 数据安全/隐私保护
Spring Boot 3.3中的优雅实践:全局数据绑定与预处理
【10月更文挑战第22天】 在Spring Boot应用中,`@ControllerAdvice`是一个强大的工具,它允许我们在单个位置处理多个控制器的跨切面关注点,如全局数据绑定和预处理。这种方式可以大大减少重复代码,提高开发效率。本文将探讨如何在Spring Boot 3.3中使用`@ControllerAdvice`来实现全局数据绑定与预处理。
61 2
|
Java 测试技术 Spring
Spring入门&控制反转(或依赖注入)&AOP的关键概念& 多配置文件&与web集成(二)
Spring入门&控制反转(或依赖注入)&AOP的关键概念& 多配置文件&与web集成
|
3月前
|
XML Java 数据库
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式
这篇文章是Spring5框架的实战教程,详细介绍了事务的概念、ACID特性、事务操作的场景,并通过实际的银行转账示例,演示了Spring框架中声明式事务管理的实现,包括使用注解和XML配置两种方式,以及如何配置事务参数来控制事务的行为。
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式
|
SQL Java 关系型数据库
Spring入门&控制反转(或依赖注入)&AOP的关键概念& 多配置文件&与web集成(一)
Spring入门&控制反转(或依赖注入)&AOP的关键概念& 多配置文件&与web集成
148 0
下一篇
无影云桌面