Spring Data JPA

JPA(Java Persistence API)是JavaEE标准中,一个类似Hibernate的持久化API,实际上和Hibernate使用起来没什么区别,只不过JPA是“JavaEE标准的”。Hibernate(JPA)虽然配置略微麻烦,但是使用起来还是比较方便的,Java类和数据库表映射配置完成后,我们写很少的HQL(JPA中是JPQL)代码就能完成比较复杂的业务功能。

然而,Spring Data JPA能够让我们几乎不写任何SQL(HQL、JPQL)就能完成相同的查询功能,尤其是配合SpringBoot使用,开发效率更是相当不错。下面简单介绍一下如何配置和使用Spring Data JPA,例子中并没有使用Web工程,只是创建了一个简单的Java工程并结合使用了Spring框架,但是原理是一样的。

提醒:

  1. Spring Data JPA虽然用法简单,但也有一些广受诟病的缺陷,比如性能优化难、用法不灵活、很多魔法操作等,在一些较严肃、可预见维护周期长的工程中慎用。
  2. Hibernate版本迭代非常快,API更是改来改去,Spring Data JPA建议整合到SpringBoot使用,否则很难搞清楚版本之间的对应关系。

引入Maven依赖

如果在单独的工程中使用Spring Data JPA需要引入如下依赖:

<!-- Spring Data JPA -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>2.0.9.RELEASE</version>
</dependency>
<!-- Hibernate作为JPA实现 -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>4.3.11.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>4.3.11.Final</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.29</version>
</dependency>
<!-- Hikari连接池 -->
<dependency>
    <groupId>hikari-cp</groupId>
    <artifactId>hikari-cp</artifactId>
    <version>2.6.0</version>
</dependency>

如果使用SpringBoot环境就简单多了,直接引入起步依赖和对应数据库驱动即可:

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

Spring配置文件

如果不使用SpringBoot,我们需要手动配置数据源、JPA实体管理器、事务管理器等,这些对象配置起来比较复杂,我们这里直接给出一个完整的例子:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:jpa="http://www.springframework.org/schema/data/jpa"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

    <context:component-scan base-package="com.gacfox.demojpa" />

    <!-- Spring Data JPA Repo对象配置 -->
    <jpa:repositories base-package="com.gacfox.demojpa.dao"
        repository-impl-postfix="Impl"
        entity-manager-factory-ref="entityManagerFactory"
        transaction-manager-ref="txManager" />

    <!-- 配置Hikari数据源 -->
    <bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
        <property name="poolName" value="mercatus_connection_pool" />
        <property name="dataSourceClassName" value="com.mysql.cj.jdbc.MysqlDataSource" />
        <property name="maximumPoolSize" value="50" />
        <property name="maxLifetime" value="60000" />
        <property name="idleTimeout" value="30000" />
        <property name="dataSourceProperties">
            <props>
                <prop key="url"><![CDATA[jdbc:mysql://127.0.0.1:3306/jpa_demo?useSSL=false&serverTimezone=GMT%2B8]]></prop>
                <prop key="user">root</prop>
                <prop key="password">root</prop>
                <prop key="prepStmtCacheSize">250</prop>
                <prop key="prepStmtCacheSqlLimit">2048</prop>
                <prop key="cachePrepStmts">true</prop>
                <prop key="useServerPrepStmts">true</prop>
            </props>
        </property>
    </bean>
    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
        <constructor-arg ref="hikariConfig" />
    </bean>

    <!-- JPA实体管理器 -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="packagesToScan" value="com.gacfox.demojpa.model" />
        <property name="persistenceProvider">
            <bean class="org.hibernate.ejb.HibernatePersistence" />
        </property>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="generateDdl" value="false" />
                <property name="database" value="MYSQL" />
                <property name="databasePlatform" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
                <property name="showSql" value="true" />
            </bean>
        </property>
        <property name="jpaDialect">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" />
        </property>
        <property name="jpaPropertyMap">
            <map>
                <entry key="hibernate.query.substitutions" value="true 1, false 0" />
                <entry key="hibernate.default_batch_fetch_size" value="16" />
                <entry key="hibernate.max_fetch_depth" value="2" />
                <entry key="hibernate.generate_statistics" value="true" />
                <entry key="hibernate.bytecode.use_reflection_optimizer" value="true" />
                <entry key="hibernate.cache.use_second_level_cache" value="false" />
                <entry key="hibernate.cache.use_query_cache" value="false" />
            </map>
        </property>
    </bean>

    <!-- 事务管理器 -->
    <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>

    <!-- 注解驱动的事务支持 -->
    <tx:annotation-driven transaction-manager="txManager" />

</beans>

SpringBoot环境下配置

SpringBoot环境下,配置都集成在application.properties项目配置文件中。许多配置在SpringBoot下都是约定的默认值,下面是一些例子:

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/jpa_demo?useSSL=false&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.database=MYSQL
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

这里实际上还有很多其他可选项,具体可以参考文档。

使用Spring Data JPA进行增删改查

下面我们编写一个例子,通过Spring Data JPA实现最简单的增删改查。

表结构准备

下面所有的例子都基于以下三个数据表:

create table t_user (
    user_id  bigint auto_increment,
    username varchar(20),
    password varchar(20),
    birthday datetime,
    primary key (user_id)
);
create table t_role (
    role_id   bigint auto_increment,
    role_name varchar(20),
    primary key (role_id)
);
create table t_user_role (
    user_role_id bigint auto_increment,
    user_id      bigint,
    role_id      bigint,
    primary key (user_role_id)
);

其中用户表和角色表具有多对多关系,多对多关系通过一张中间表来表达。Java类和数据库表的映射关系这里使用JPA注解进行配置(有关JPA注解的内容在Hibernate章节都介绍过了)。

实体类编写

User.java

package com.gacfox.demojpa.model;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Set;

@Entity
@Table(name = "t_user")
public class User implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long userId;
    @Column(name = "username")
    private String username;
    @Column(name = "password")
    private String password;
    @Column(name = "birthday")
    private Date birthday;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinTable(name = "t_user_role", joinColumns = { @JoinColumn(name = "user_id") }, inverseJoinColumns = {
            @JoinColumn(name = "role_id") })
    private Set<Role> roleSet;

    // ... 构造函数、GET/SET、toString等方法,为了节省篇幅此处省略
}

Role.java

package com.gacfox.demojpa.model;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;

@Entity
@Table(name = "t_role")
public class Role implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "role_id")
    private Long roleId;
    @Column(name = "role_name")
    private String roleName;

    @ManyToMany(mappedBy = "roleSet", fetch = FetchType.LAZY)
    private Set<User> userSet;

    // ... 构造函数、GET/SET、toString等方法,为了节省篇幅此处省略
}

JpaRepository接口查询

下面代码定义了一个接口,实现了一些常用的Dao功能。

UserRepository.java

package com.gacfox.demojpa.dao;

import org.springframework.data.jpa.repository.JpaRepository;
import com.gacfox.demojpa.model.User;
import java.util.List;

public interface UserRepository extends JpaRepository<User, Long> {

    /**
     * 根据用户ID查询用户
     * 
     * @param userId 用户ID
     * @return 用户对象
     */
    public User findByUserId(Long userId);

    /**
     * 根据用户名查询用户
     * 
     * @param username 用户名
     * @return 用户对象
     */
    public User findByUsername(String username);

    /**
     * 根据用户名和密码查询用户
     * 
     * @param username 用户名
     * @param password 密码
     * @return 用户对象
     */
    public User findByUsernameAndPassword(String username, String password);

    /**
     * 模糊查询用户
     * 
     * @param pattern 匹配字符串,如[prefix]%这种形式
     * @return 用户对象列表
     */
    public List<User> findByUsernameLike(String pattern);
}

注意代码中UserRepository这个类的定义,它继承了JpaRepository,我们不需要写任何实现类,Spring Data JPA已经为我们做好了,我们只需要在我们的Service层中调用UserRepository即可。

这看起来比较神奇,但实际上Spring Data JPA会通过我们定义的方法名自动生成数据库查询,规则是这样的:

  • 所有查询方法名以findBy开头
  • 查询条件写在findBy后面,名字需要和Java实体类的字段名相对应,如findByUsername
  • 多个查询条件可以用AndOr等连接,如findByUsernameAndPassword
  • 模糊查询在条件字段后加Like,如findByUsernameLike

上述规则能够覆盖80%的查询操作了,这就是Spring Data JPA的方便之处。

自定义JpaRepository实现

当然,如果查询条件十分复杂,上面这种写法不能满足的时候,我们也可以写接口的实现类,那就和Hibernate(JPA)没什么区别了,下面例子中我们自己实现了一个接口用于查询。

UserRepository.java

public Role findByRoleName2(String roleName);
package com.gacfox.demojpa.dao;

import com.gacfox.demojpa.model.Role;
import org.springframework.beans.factory.annotation.Autowired;
import javax.persistence.EntityManager;
import javax.persistence.Query;

public class UserRepositoryImpl {
    @Autowired
    private EntityManager entityManager;

    public Role findByRoleName2(String roleName) {
        String jpql = "select r from Role r where r.roleName=:roleName";
        Query query = entityManager.createQuery(jpql);
        query.setParameter("roleName", roleName);
        return (Role) query.getSingleResult();
    }
}

注意实现类的写法,我们没有继承任何接口,只是以接口名Impl命令了我们的实现类,并且实现类和接口在同一个包中,然后定义了一个相同名字的Java方法的实现,如果我们使用implements实现RoleRepository,我们需要同时实现大量其他方法(RoleRepository以及其父接口中定义的所有方法)才能编译通过,因此SpringDataJPA内部进行了一些处理,允许我们不使用implements指定实现的接口,而保持接口和实现类按照约定命名即可。

这种写法有些脱离Java语法的一般常识了,个人认为是一个糟糕的设计,除非必要否则强烈不建议用。

插入的写法

插入使用save()方法,和Hibernate(JPA)一样,例子代码如下:

Role role = new Role("student");
roleRepository.save(role);

删除的写法

我们执行删除操作时,通常也是要附加一些查询条件的,删除的写法和查询差不多:

/**
 * 根据角色名删除角色
 * @param roleName 角色名
 * @return 受影响行数
 */
public int deleteByRoleName(String roleName);

修改的写法

修改的用法也和Hibernate(JPA)一样,临时态对象修改后使用save()保存为持久态,Java对象的修改就会持久化到数据库中了。

QBE查询

QBE(Query By Example)是一种查询方式,能够直接通过模板对象,从数据库中匹配符合条件的记录,形成结果集。

下面是一段从项目中摘取的一段QBE查询的例子代码,主要实现了分页和条件查询:

@Override
public Map<String, Object> getFileRecordList(Integer pageSize, Integer currentPage, String namePattern, Integer isExist) {
  // 排序分页
  Sort sort = new Sort(Sort.Direction.ASC, "fileId");
  PageRequest pageRequest = PageRequest.of(currentPage - 1, pageSize, sort);
  // QBE查询
  FileRecord fileRecord = new FileRecord();
  fileRecord.setName(namePattern);
  fileRecord.setIsExist(isExist);
  ExampleMatcher matcher = ExampleMatcher.matching()
      .withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains())
      .withMatcher("isExist", ExampleMatcher.GenericPropertyMatchers.exact());
  Example<FileRecord> example = Example.of(fileRecord, matcher);

  Page<FileRecord> resultPage = fileRecordRepository.findAll(example, pageRequest);

  Map<String, Object> resultMap = new HashMap<>();
  // 所有记录
  resultMap.put("records", resultPage.getContent());
  // 当前页码
  resultMap.put("currentPage", currentPage);
  // 总记录数
  resultMap.put("totalElements", resultPage.getTotalElements());
  return resultMap;
}

Criteria高级动态查询

有些很复杂的动态查询,JpaRepository和QBE都不能很好的满足我们的需求,而使用拼接JPQL或SQL也是相当麻烦而且不可取的,这时我们可以使用Criteria查询。在Spring Data JPA中,对Criteria操作进行了封装,使用起来还算简单。

想要使用Criteria查询的Repository接口,需要继承JpaSpecificationExecutor<>接口,在repository实例上调用findAll()方法时,传入一个Specification<>对象。具体我们看一个例子,下面例子是来自实际项目中的一段代码,实现了共5个条件的动态查询,查询参数类型包括字符串模糊查询、字符串精确查询、日期比较查询、整数查询:

查询参数封装类型:QueryParam.java

public class QueryParam {
    private Date startTime;
    private Date endTime;
    private String keyword;
    private Integer publicStatus;
    private Integer starStatus;

    private Integer pageSize;
    private Integer currentPage;

    // ... 构造函数、GET/SET、toString等方法,为了节省篇幅此处省略
}

动态查询的写法:

@Override
public Map<String, Object> queryDiaryByPageAndParam(QueryParam queryParam) {
  // 排序分页
  Sort sort = new Sort(Sort.Direction.ASC, "diaryId");
  PageRequest pageRequest = PageRequest.of(queryParam.getCurrentPage() - 1, queryParam.getPageSize(), sort);
  // 动态查询
  Page<Diary> queryResult = diaryRepository.findAll(new Specification<Diary>() {
    @Override
    public Predicate toPredicate(Root<Diary> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
      List<Predicate> predicateList = new ArrayList<>();

      // 时间字段大于起始时间
      Predicate startTimePredicate = null;
      if (queryParam.getStartTime() != null) {
        startTimePredicate = criteriaBuilder.greaterThanOrEqualTo(root.<Date>get("createTime"), queryParam.getStartTime());
        predicateList.add(startTimePredicate);
      }

      // 时间字段小于结束时间
      Predicate endTimePredicate = null;
      if (queryParam.getEndTime() != null) {
        endTimePredicate = criteriaBuilder.lessThanOrEqualTo(root.<Date>get("createTime"), queryParam.getEndTime());
        predicateList.add(endTimePredicate);
      }

      // 标题和内容中包含指定关键字
      Predicate keywordPredicateInTitle = null;
      Predicate keywordPredicateInContent = null;
      if (queryParam.getKeyword() != null && !"".equals(queryParam.getKeyword())) {
        keywordPredicateInTitle = criteriaBuilder.like(root.<String>get("title"), "%" + queryParam.getKeyword() + "%");
        keywordPredicateInContent = criteriaBuilder.like(root.<String>get("content"), "%" + queryParam.getKeyword() + "%");

        Predicate keywordPredicate = criteriaBuilder.or(keywordPredicateInTitle, keywordPredicateInContent);
        predicateList.add(keywordPredicate);
      }

      // 具有指定的公开状态
      Predicate publicStatusPredicate = null;
      if (queryParam.getPublicStatus() != null) {
        publicStatusPredicate = criteriaBuilder.equal(root.<Integer>get("isPublic"), queryParam.getPublicStatus());
        predicateList.add(publicStatusPredicate);
      }
      // 具有指定的加星状态
      Predicate starStatusPredicate = null;
      if (queryParam.getStarStatus() != null) {
        starStatusPredicate = criteriaBuilder.equal(root.<Integer>get("isStar"), queryParam.getStarStatus());
        predicateList.add(starStatusPredicate);
      }
      // 动态查询条件合并
      int predicateSize = predicateList.size();
      if (predicateSize > 0) {
        Predicate predicate = criteriaBuilder.and((Predicate[]) predicateList.toArray(new Predicate[predicateSize]));
        // 将查询条件提交给查询计划
        query.where(predicate);
      }
      return null;
    }
  }, pageRequest);

  Map<String, Object> resultMap = new HashMap<>(64);
  // 所有记录
  resultMap.put("records", queryResult.getContent());
  // 当前页码
  resultMap.put("currentPage", queryParam.getCurrentPage());
  // 总记录数
  resultMap.put("totalElements", queryResult.getTotalElements());
  return resultMap;
}

总结

Spring Data JPA非常适合配合SpringBoot快速实现简单、不追求性能的需求;相反,在SQL查询十分复杂、性能要求高的场景,Spring Data JPA就不适用了,因为Spring Data JPA基于方法名自动实现JPQL查询难以表达查询参数非常多、非常复杂的情况,而编写实现类就和直接使用Hibernate(JPA)差不多麻烦了;除此之外,毕竟是基于Hibernate(JPA),性能优化也是个麻烦的问题,需要性能的情况推荐使用JDBC、MyBatis的方式。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap