Spring 加强版 ORM 框架 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,也就是今天的主题,关于上面提到的这些技术,可以用如下的图示来表示。
在这里插入图片描述

认识 spring-data-jpa

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

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

在这里插入图片描述由于 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 模块都支持的接口,提供了基本的增删改查方法,QueryByExampleExecutorspring-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,还会有非常智能的代码提示。
在这里插入图片描述
更多查询关键字,可以参考 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);
}

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

分页与排序

如果需要分页或者排序,可以将 PageableSort 参数附加在方法参数后面,同时支持 @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 包含了 HibernateHikariCP 的依赖,Spring 会自动配置 DataSourceEntityManagerFactoryTransactionManager,以及启用 JPA Repository bean 注册与查找的功能,当然了,必须的数据库驱动还是需要用户手动提供的。

总结

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

  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 22
    评论
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大鹏cool

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值