分类
persistence

在JPA中的定义索引

译者注

原文地址:https://www.baeldung.com/jpa-indexes
实例代码:https://github.com/eugenp/tutorials/tree/master/persistence-modules/java-jpa-3

1. 概述

本文我们将讨论使用JPA的@Index注解来定义索引。在实例中,我们首先讲解如何使用JPA和Hiernage来定义第一个索引。接下来讲解更多关于自定义注解的知识。

2. @Index 注解

数据库索引是一种数据结构,它以增加的写入和存储空间为代价,提高了对表进行数据检索操作的速度。通常来说,它是从某个单表中选择的数据库的副本。我们应该合理的为数据表创建索引来提高数据库的查询性能。

JPA允许我们在代码中使用@Index注解来定义数据表索引。该注解能够实现在创建(更新)数据表时自动为数据表添加索引。

接下来我们看一下如何使定义索引:

2.1. javax.persistence.Index

自JPA2.1版本,JPA增加了java.prsistence.Index来允许我们为数据表创建注解:

@Target({})
@Retention(RUNTIME)
public @interface Index {
    String name() default "";
    String columnList();
    boolean unique() default false;
}

如上代码所示,@Index注解中仅有columnList属性是必填的。后面我们将具体使用该注解来具体的查看各个属性的用法。

2.2. JPA 和 Hibernate

我们常弄不清JPA与Hibernate的关系。我们知道JPA仅仅是一个规范,它除了做了一些规定提供了一些愿景(也可以说模板、蓝图)以外,其它什么也没做。而要其使愿景便成事实,则需要有人来做实际的工作将其实现。在Spring中使用了Hibernate来完成了这个愿景,你可以参考本文来进一步学习。当然了,除了 Hibernate以外还有其它的JPA实现,比如EclipseLink。所以JPA就好像面向对象思想中的接口,而HibernateEclipseLink则都是这个接口的具体实现

值的注意的是由于JPA添加索引支持比较晚,而在这之前多个ORM框架均有自己支持索引的方法。这使得在JPA统一规范以前,出现了各个ORM各自为政的情况。比如Hibernate就有自己的org.hibernate.annotations.Index注解。所以我们在使用索引注解时,一定上要确认使用的是JPA下的注解 ---- java.prsistence.Index(而不是其他框架中的同名注解)。

简单的了解一些技术背景了,让我们正式开始。

3. 定义@Index

在定义索引前,我们需要了解一些如何定义JPA实体的相关知识。

我们实现一个Student学生实体如下:

/**
 * 学生.
 *
 * @author panjie
 */
@Entity
@Table
public class Student implements Serializable {
  @Id
  @GeneratedValue
  private Long id;

  /**
   * 姓名.
   */
  private String name;

  /**
   * 学号.
   */
  private String sno;

  // setters getters
}

接下来,我们在@Table注解中加入相关的@Index注解。

@Table(indexes = @Index(columnList = "name"))

如上我们创建了第一个基于name的索引,此时数据表在初始化时,控制台的日志信息中将对应生成以下Sql代码:

// 中间的值是自动生成的,不会完全一致
[main] DEBUG org.hibernate.SQL -
  create index IDX2gdkcjo83j0c2svhvceabnnoh on Student (name)

接下来我们索引的其他属性和功能。

3.1. 索引名称

不难看出,索引必须有一个名称。默认情况下我们没有指定名称,那么将会使用自动生成的值。
如果我们需要自定义一个名称,只需要修改name属性:

@Index(name = "test_index", columnList = "name")

如代码所示,我们创建了一个名为test_index的索引。
在数据表初始化时,日志就会变成:

[main] DEBUG org.hibernate.SQL -
  create index test_index on Student (name)

此外,我们可以通过在名称中指定模式(schema)的名称,以便在不同的模式(schema)中创建索引:

@Index(name = "schema2.test_index", columnList = "name")

3.2. 在多个列上定义一个@Index

columnList语法如下:

column ::= index_column [,index_column]*
index_column ::= column_name [ASC | DESC]

刚才我们提到,索引是对于一个列的索引,也必须包括列名。然而,我们也可以在单个索引中指定多个列。
方法就是用逗号来分隔名称:

@Index(name = "mulitIndex1", columnList = "name, phoneNumber")

@Index(name = "mulitIndex2", columnList = "phoneNumber, name")

生成的日志:

[main] DEBUG org.hibernate.SQL -
  create index mulitIndex1 on Student (name, phoneNumber)

[main] DEBUG org.hibernate.SQL -
  create index mulitIndex2 on Student (phoneNumber, name)

需要注意的是,如果我们定义多列索引,索引的前后顺序必须相同。如果顺序变化,即使使用的同一组列,也是两个不同的索引,如代码所示。

3.3. @Index 索引排序(Order

我们还可以在列名column_name之后指定ASC(升序)或DESC(降序),来控制索引值的排列顺序:

@Index(name = "mulitSortIndex", columnList = "name, phoneNumber DESC")

日志:

[main] DEBUG org.hibernate.SQL -
  create index mulitSortIndex on Student (name, phoneNumber desc)

3.4. 索引唯一性(Unique

还有一个参数是unique属性,它用来定义索引是否唯一。“唯一”索引确保索引的字段不存储重复值(就像平时常见的ID或者学号等等)。
默认情况下它是false,如果想启用唯一性,可以这样声明:

@Index(name = "uniqueIndex", columnList = "name", unique = true)

日志:

[main] DEBUG org.hibernate.SQL -
  alter table Student add constraint uniqueIndex unique (firstName)

当我们在某个列上使用唯一索引的时候,就相当于再这个列上添加了一个唯一性约束。它的作用和“在@Column注解上使用约束”是相似的。
但由于@Index可以声明多个列的唯一约束(相当于联合主键,单个列的数据允许重复,只有约束中的所有列的数据都重复,才不被允许),使用@Index要优于使用@Column:

@Index(name = "uniqueMulitIndex", columnList = "firstName, lastName", unique = true)

3.5. 在单个实体上定义多个@Index索引

到目前为止,我们已经学习了索引的各个参数的功能。
而事实上,我们可以在一个实体上定义多个索引,多个索引的定义方法是——在大括号中重复使用@Index注解:

@Entity
@Table(indexes = {
  @Index(columnList = "firstName"),
  @Index(name = "test_index", columnList = "name"),
  @Index(name = "mulitIndex1", columnList = "name, phoneNumber"),
  @Index(name = "mulitIndex2", columnList = "phoneNumber, name"),
  @Index(name = "mulitSortIndex", columnList = "name, phoneNumber DESC"),
  @Index(name = "uniqueIndex", columnList = "name", unique = true),
  @Index(name = "uniqueMulitIndex", columnList = "name, phoneNumber", unique = true)
})
public class Student implements Serializable

此外,我们还可以为同一组列创建多个索引。

3.6. 主键

当我们谈论索引时,必须要考虑主键。因为被实体管理器EntityManager来管理的每个实体都必须指定一个主键。
一般来说,主键是一种特定类型的唯一索引,与其他索引不同的是,主键不需要用前面介绍的@Index来声明,因为@Id 注解自动实现了这个功能。

3.7. 对于非实体(Entity)对象使用@Index

我们需要知道的是,@Index注解不仅仅能用在表(@Table)上,也能用于某些其他的对象,例如@SecondaryTable(多表联合)、@CollectionTable(集合表)、@JoinTable(连接表)、@TableGenerator(表生成器),这些知识不再详细讲解,请查看javax.persistence JavaDoc

4. 结论

本文中我们讨论了如果使用JPA中的@Index索引,包括更改名称、包含的列、顺序、以及唯一性。后面我们讨论了主键,以及如何在其他对象中声明索引。

简单总结一下:

  1. 索引名称name,不使用名称时会自动生成
  2. 多个列columnList,列的顺序必须一致
  3. 排序ASCDESC
  4. 唯一性unique = true
  5. 主键不需要使用@Index,因为@Id完成了这个功能

本文中的示例可在 GitHub 上找到。

分类
Spring Data Spring Persistence

Spring Data JPA设置字段默认值的两种方法

1. 简介

本文将介绍如何在JPA中定义数据列的默认值。
通常有两种方式来实现默认值的定义:在实体类中设置默认值以及使用JPA注解直接操作数据表。

2. 实体类

第一种方法是直接在实体中定义:

@Entity
public class User {
    @Id
    private Long id;
    private String firstName = "Code demo";
    private Integer age = 25;
    private Boolean locked = false;
}

此时,当我们使用new关键字来实例化实体时,实体的各个字段将以默认值填充:

@Test
void saveUser_shouldSaveWithDefaultFieldValues() {
    User user = new User();
    user = userRepository.save(user);

    assertEquals(user.getName(), "Code demo");
    assertEquals(user.getAge(), 25);
    assertFalse(user.getLocked());
}

使用方法设置的默认值并未在数据表中定义中生效,查看相应的SQL语句如下:

create table user
(
    id     bigint not null constraint user_pkey primary key,
    name   varchar(255),
    age    integer,
    locked boolean
);

由以上SQL可知,该方法实际上并没有为数据表中的字段设置默认值,所以如果我们在代码中手动的将某个字段设置为null,并不会引发任何异常:

@Test
void saveUser_shouldSaveWithNullName() {
    User user = new User();
    user.setName(null);
    user.setAge(null);
    user.setLocked(null);
    user = userRepository.save(user);

    assertNull(user.getName());
    assertNull(user.getAge());
    assertNull(user.getLocked());
}

3. 定义数据表

在JPA中,我们可以使用@Column注解的columnDefinition参数来定义数据表字段的默认值:

@Entity
public class User {
    @Id
    Long id;

    @Column(columnDefinition = "varchar(255) default 'Code demo'")
    private String name;

    @Column(columnDefinition = "integer default 25")
    private Integer age;

    @Column(columnDefinition = "boolean default false")
    private Boolean locked;
}

使用上述方法定义后,JPA将对应生成以下SQL语句:

create table user
(
    id     bigint not null constraint user_pkey primary key,
    name   varchar(255) default 'Code demo',
    age    integer      default 35,
    locked boolean      default false
);

此时将未设置某个字段的值时,该字段将使用默认值填充:

@Test
void saveUser_shouldSaveWithDefaultSqlValues() {
    User user = new User();
    user = userRepository.save(user);

    assertEquals(user.getName(), "John Snow");
    assertEquals(user.getAge(), 25);
    assertFalse(user.getLocked());
}

值得注意的是:使用该方案在新建数据时,我们无法将某个字段的值设置null。因为如果我们将某个字段的值设置为null,则在进行数据保存操作时,将会以默认值来覆盖null值。

4. 总结

本文着重介绍了两个设置数据字段默认值的方法,在实际的应用中,还需要结合实际的情景来选择具体合适哪种方案。

https://www.baeldung.com/jpa-default-column-values

分类
Spring Data

Spring Data JPA 创建、组合自定义仓库

1. 简介

在对真实世界或操作流程进行建模时,建立领域驱动设计domain-drven design(DDD)风格的数据仓库是个不错的选择。正是基于此,我们在数据访问层选择了Spring Data JPA。

如果您对Spring Data JPA还不太了解,推荐您首先阅读Spring Data JPA简介一文。

在本文中,我们将重点介绍创建自定义数据仓库以及如果组合使用自定义的数据仓库。

2. MAVEN依赖

在Spring自版本5开始支持创建及组合自定义数据仓库,添加Spring Data JPA依赖如下:

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>

如果你使用了Spring Boot,应该移除上述依赖中的version版本号(本文代码示例基于Spring Boot)。

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

无疑还要为Spring Data JPA准备一个数据库,比如开源数据库mysql。但在开发测试环境中我们会经常使用内存数据库H2来代替生产环境中的mysql,该数据不需要任何环境的特性极大程度上提长了开发测试效率,当然也特别适用于学习交流中的代码演示。添加依赖如下:

<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<scope>runtime</scope>
</dependency>

3. 准备工作

3.1 Hibernate实现

Spring Data JPA默认使用了Hibernate。以致于有些时候,我们误认为为Spring Data JPA就是Hibernate,但本质上来讲Spring Data JPA其实是一种规范,Hibernate则是该规范的一种实现。

在实际的应用中,除Hibernate以外还有其它Spring Data JPA实现可供选择。比如:在Spring中使用EclipseLink

3.2 默认仓库

在多数情况下,我们并不需要写过多的查询语句便可以轻松的实现一般的数据操作功能。在这里,我们仅需要创建一个类似如下的接口:

public interface StudentRepository extends JpaRepository<Student❶, Long❷> {
}
  • ❶实体类
  • ❷主键类型

该接口继承了JpaRepository接口,从而自动实现了:CRUD、分页、排序等基本的数据操作功能。

此外Spring Data JPA还提供了一种根据方法名称自动实现查询功能的机制。比如查询姓名为X的学生列表可以定义如下方法:

public interface StudentRepository extends JpaRepository<Student, Long> {
    List<Student> findByName(String name);
}

Spring Data JPA还提供了更多的查询方法,详情请参阅官方网站

3.3 自定义数据仓库

当Spring Data JPA提供的内置方法无法满足复杂的业务需求时,可以通过自定义数据仓库的方法来扩展原数据仓库。

比如使用自定义的CustomStudentRepository来扩展StudentRepository:

public interface StudentRepository extends JpaRepository<Student, Long>, CustomStudentRepository {
}

CustomStudentRepository中定义如下:

/**
 * 自定义学生仓库,该仓库做为为基础仓库的补充
 */
public interface CustomStudentRepository {
    /**
     * 删除学生
     *
     * @param student 学生
     */
    void deleteStudent(Student student);
}

最后,新建CustomStudentRepositoryImpl类并实现CustomStudentRepository。

public class CustomStudentRepositoryImpl implements CustomStudentRepository {
    @PersistenceContext
    EntityManager entityManager;

    @Override
    public void deleteStudent(Student student) {
        this.entityManager.remove(student);
    }
}

需要注意的是,此实现类的命名必须为: 接口名称 + Impl后缀。因为只有这样Spring Data JPA才能够正确地扫描并使用该实现类。如果你想修改后缀关键字Impl(比如修改为CustomImpl),则可以进行如下配置:

使用XML配置示例:

<repositories base-package="club.codedemo.springdatacomposablerepositories.repository" repository-impl-postfix="CustomImpl" />

JAVA配置示例:

@EnableJpaRepositories(
        basePackages = "club.codedemo.springdatacomposablerepositories.repository",
        repositoryImplementationPostfix = "CustomImpl")

4. 组合多个自定义仓库

在Spring 5版本以前,我们是没有办法将多个自定义仓库组合到一起来使用的。

Spring 5开始支持将多个自定义仓库组合在一起来使用,比如我们再增加一个自定义仓库:

public interface CustomTeacherRepository {
    void deleteTeacherByStudent(Student student);
}

然后便可以采用多实现的方法将CustomTeacherRepository与原CustomStudentRepository组合在一起来使用了:

public interface StudentRepository extends
        JpaRepository<Student, Long>,
        CustomStudentRepository,
        CustomTeacherRepository {

此时StudentRepository的实例将同时拥有CustomStudentRepository以及CustomTeacherRepository定义的方法,并在某个方法方法被调用时,聪明地调用不同的实现类中的具体方法而实现相应的功能。

5. 处理岐义

当我们继承多个接口时,便不可避免的遇到同一个方法被不同的接口重复声明的情况。当这种情况发生时,便在“应该调用哪个具体实现类”上产生了岐义。

比如我们同时在StudentRepository、CustomStudentRepository以及CustomTeacherRepository上分别定义findByIdOrName()方法:

List<Student> findByIdOrName(Long id, String name);

我们以StudentRepository为例,来查看岐义的处理原则。:

public interface StudentRepository❸ extends
        JpaRepository<Student, Long>❹,
        CustomStudentRepository❶,
        CustomTeacherRepository❷ {

当发生岐义时,调用的优先级为:❶ > ❷ > ❸ > ❹

总结:自定义的仓库❶❷中的方法执行优先级最高,所以❶❷ > ❸❹;❶CustomStudentRepository位于❷CustomTeacherRepository之前,所以❶优先级高于❷。在❸StudentRepository中定义的方法优先级高于Spring Data JPA默认实现❹JpaRepository中定义的方法。

6. 总结

本文对Spring Data JPA创建并使用自定义数据仓库进行介绍。我们看到,Spring的自定义仓库模式使得我们可以自由地对数据仓库进行扩展。

创建自定义可组合仓库的模式无疑将对数据仓库的灵活性有大幅的提升。

分类
persistence

JPA Querydsl 入门

1. 概述

Querydsl是一款开源的JAVA框架,用于构造安全的SQL查询语句,在使用的风格上更贴近于SQL。本文将讨论其集成在JPA中的使用方法。Querydsl起初仅提供了对Hibernate中的HQL的支持,但如今其已经全面支持JPA, JDO, JDBC, Lucene, Hibernate Search, MongoDB, Collections 以及 RDFBean 了。

2. 准备

 在Spring Boot项目中编辑pom.xml文件,添加以下信息:

<properties>
	...
	<querydsl.version>4.1.3</querydsl.version>
</properties>
<dependencies>
        ...

	<dependency>
		<groupId>com.querydsl</groupId>
		<artifactId>querydsl-apt</artifactId>
		<version>${querydsl.version}</version>
		<scope>provided</scope>
	</dependency>

	<dependency>
		<groupId>com.querydsl</groupId>
		<artifactId>querydsl-jpa</artifactId>
		<version>${querydsl.version}</version>
	</dependency>
</dependencies>
<build>
	<plugins>
               ...

		<plugin>
			<groupId>com.mysema.maven</groupId>
			<artifactId>apt-maven-plugin</artifactId>
			<version>1.1.3</version>
			<executions>
				<execution>
					<goals>
						<goal>process</goal>
					</goals>
					<configuration>
						<outputDirectory>target/generated-sources/java</outputDirectory>
						<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
					</configuration>
				</execution>
			</executions>
		</plugin>
	</plugins>
</build>

如上所示,我们使用的Querdsl版本为4.1.3。添加了两个依赖以及一个MAVEN APT插件。该插件中声明的com.querydsl.apt.jpa.JPAAnnotationProcessor的作用是:查找以 javax.persistence.Entity注解的类,并自动生成其对应的用于Querdsl查询的类。

3. 小试牛刀

Querdsl根据实体来构造查询类型,然后在查询的过程中直接应用查询类型的属性,从而保证了在编译的阶段便能够发现其拼写错误,被认为是安全的。

相较于JPA Criteria Queries中在查询时指定字符串而造成不安全的特性,Querdsl在安全性上更加具有优势。

同时,Querdsl在查询时也保持了较高的一致性。下面,让我们分步查看Querds是如何工作的。

3.1 建立实体

新建学生Student实体如下:

/**
 * 学生
 */
@Entity
public class Student {
    @Id
    @GeneratedValue
    private Long id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 学号
     */
    private String no;

    /**
     * 体重
     */
    private Integer weight;

  
    // setter and getter
}

此时执行mvn compile,将在target文件夹对应的包中对应生成供查询使用的QStudent类。

我们可以通过获取QStudent中的静态变量student来获取一个QStudent的实例:

QStudent qStudent = QStudent.student;

如果你使用的是IDEA编辑器,可能会接收到找不到类的错误,此时可以尝试找到pom.xml,点击右键 -> maven -> Generate Sources and Update Folders。

3.2 使用JPAQueryFactory构建查询

我们可以如下代码来获取一个JPAQueryFactory实例:

JPAQueryFactory query = new JPAQueryFactory(entityManager);

注意:上述代码中的entityManager是由JPA中的EntityManager

假设需要查找姓名为X的学生,则代码为:

/**
 * 查询姓名为X的学生
 * @param name 学生姓名
 * @return
 */
Student findByName(String name) {
    JPAQueryFactory query = new JPAQueryFactory(entityManager);

    QStudent qStudent = QStudent.student;

    return query.selectFrom(qStudent)
         .where(qStudent.name.eq(name))
         .fetchOne();
}

上述代码中:selectFrom指定了查询的数据表为student,where()方法规定的查询的条件,fetchOne()返回相关的某条学生记录。

多条件组件查询示例:

query.selectFrom(qStudent)
                .where(qStudent.name.eq(name), qStudent.no.eq(no))
                .fetchOne();

where()方法可能接收多个表达式(查询条件),在查询过程中对多个表达式(查询条件)做组合查询。此外,也可以调用表达式的and方法来完成组合查询:

query.selectFrom(qStudent)
                    .where(qStudent.name.eq(name).and(qStudent.no.eq(no)))
                    .fetchOne();

如果将上述代码转换为JPQL语言,则相当于:

select student form Student as student where student.name = ?1 and student.no = ?2

此外,还支持or查询:

/**
 * 按姓名或学号查询学生
 * 满足任一条件即可
 *
 * @param name 姓名
 * @param no   学号
 * @return
 */
List<Student> findByNameOrNo(String name, String no) {
    JPAQueryFactory query = new JPAQueryFactory(entityManager);
    QStudent qStudent = QStudent.student;

    return query.selectFrom(qStudent)
                .where(qStudent.name.eq(name).or(qStudent.no.eq(no)))
                .fetch();
}

4. 排序及聚合查询

4.1 排序

使用orderBy方法来实现按指定字段排序:

/**
 * 按体重排序
 *
 * @return
 */
List<Student> findAllOrderByWeight() {
    JPAQueryFactory query = new JPAQueryFactory(entityManager);
    QStudent qStudent = QStudent.student;

    return query.selectFrom(qStudent)
                .orderBy(qStudent.weight.asc())
                .fetch();
}

4.2 聚合

querydsl支持Sum, Avg, Max, Min等聚合函数,比如查询出所有学生中最大的体重:

int maxWeight = query.selectFrom(qStudent)
            .select(qStudent.weight.max())
            .fetchFirst();

4.3 GroupBy

当然也支持groupBy,比如接学生的体重进行分类:

query.select(qStudent.weight)
                    .from(qStudent)
                    .groupBy(qStudent.weight)
                    .fetch();

5. 单元测试

单元测试是保证代码正确的高效有力的保障手段,为此我们使用data.sql创建了4个学生,并新建如下单元测试进行验证:

package club.codedemo.querydslwithjpatutorial;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@SpringBootTest
class StudentDaoTest {

    @Autowired
    StudentDao studentDao;

    @Test
    void findByName() {
        Assertions.assertEquals(1L,
                this.studentDao.findByName("zhangsan").getId());
    }

    @Test
    void findByNameAndNo() {
        Assertions.assertEquals(1L,
                this.studentDao.findByNameAndNo("zhangsan", "200001").getId());
    }

    @Test
    void findByNameAndNo1() {
        Assertions.assertEquals(1L,
                this.studentDao.findByNameAndNo1("zhangsan", "200001").getId());
    }

    @Test
    void findByNameOrNo() {
        Assertions.assertEquals(2,
                this.studentDao.findByNameOrNo("zhangsan", "200002").size());
    }

    @Test
    void findAllOrderByWeight() {
        List<Student> students = this.studentDao.findAllOrderByWeight();

        Assertions.assertEquals(56, students.get(0).getWeight());
        Assertions.assertEquals(65, students.get(students.size() - 1).getWeight());
    }

    @Test
    void getMaxWeight() {
        Assertions.assertEquals(65, this.studentDao.getMaxWeight());
    }

    @Test
    void groupByWeight() {
        Assertions.assertEquals(3,
                this.studentDao.groupByWeight().size());
    }
}

6. 总结

本文对Spring Boot JPA中使用Querydsl进行了初步介绍。Querydsl类型安全、语法简短、贴近于SQL的特点,使其很好做为JPA查询的一种补充。更多用户请参考Querydsl简介

分类
Spring Data

Spring Data JPA @Query

1. 概述

Spring Data提供了多种数据查询方法,本文我们将讨论使用Spring Data JPA中的@Query注解的方式。该注解同时支持JPQL以及SQL语句。不过在处理一些动态查询时,@Query注解有时候便力不从心了,为弥补该缺憾,在文章的后半部分将给出如何使用JPA Criteria API进行动态查询。

JPQL(Java Persistence Query Language) 是Java持久层查询语言。 JPQL和SQL的语法接近,但不同于SQL直接查询数据表,JPQL是对JPA实体来进行查询。最终JPQL会根据不同的底层数据库转换为对应的SQL语句。因此,使用JPQL可以减小因数据库版本差异造成的影响。

2. select语句

可以在Spring Data中的仓库层定义查询方法并使用@Query注解标识以实现特定的数据查询,@Query注解中的value属性可以接收JPQL或SQL语句,当被注解的方法被调用时,相应的JPQL或SQL语句则会对应并执行,从而达到自定义数据查询的目的。

在优先级方面,@Query注解的优先级大于@NameQuery注解或是在orm.xml文件定义的查询映射。

在Spring Data JPA中,往往会将对数据的所有操作都习惯性的集中到某个仓库接口(类)中,@Query注解可以应用到数据仓库的方法中,所以在选型时只要@Query注解能实现的,我们应该优先使用。

2.1 JPQL查询

默认情况下@Query注解使用JPQL做为查询语言。让我们看看如何使用JPQL来查询出状态为1的所有学生:

@Query("SELECT u FROM User u WHERE u.status = 1")
Collection<User> findAllActiveUsersUsingJPQL();

2.2 Native原生查询

除使用JPQL语句外,还可以定义原生的SQL语句。使用原生语句时,仅需要将nativeQuery属性设置为true:

@Query(value = "SELECT * FROM USER u WHERE u.status = 1", nativeQuery = true)
Collection<User> findAllActiveUsersUsingNative();

3. 在查询中定义排序方式

我们在可以在@Query注解下的方法中传入类型为Sort的额外参数以实现排序的目的。

3.1 使用JPA提供的方法进行排序

JPA内置了像findAll(Sort)这样的开箱即用的方法,我们可以直接使用它在参数中传递Sort进行实现排序功能:

userRepository.findAll(Sort.by(Sort.Direction.ASC, "name")

但需要注意的是JPA中接收的Sort.by方法不能够使用诸如LENGTH之类的函数,如果我们传入如下的Sort,则会得到一个异常:

userRepository.findAll(Sort.by("LENGTH(name)"))

当我们执行上面的代码时,收到异常为:

org.springframework.data.mapping.PropertyReferenceException: No property LENGTH(name) found for type User!

未找到类型为User的属性lENGTH(name)

如你如见,将字符串LENGTH(name)做为参数传给Sort.by()方法时,LENGTH(name)并没有被当成一个特殊的方法来对待,而被当前了普通的属性。由于User类中并无lENGTH(name)属性,所以发生了上述异常。

3.2 JPQL

当我们使用JPQL作为查询语言时,JPA同样可以很轻松的处理排序问题。方法与上一小节相同,同样是直接传入一个Sort类型即可:

@Query(value = "SELECT u FROM User u")
List<User> findAllUsersSortUsingJPQL(Sort sort);

比如我们想按User的name属性对结果进行排序:

userRepository.findAllUsersSortUsingJPQL(Sort.by("name"))

与JPA不的是,@Query注解支持Sort中的诸如LENGTH之类的函数名,比如要实现按用户名称的长度来获取用户的排序列表

userRepository.findAllUsersSortUsingJPQL(JpaSort.unsafe("LENGTH(name)"));

创建Sort时,使用JpaSort.unsafe()至关重要。如果直接使用Sort.by,则将收到调用原生findAll(Sort.by("LENGTH(name)"))一模一样的异常 :

Sort.by("LENGTH(name)")

在使用@Query注解时,Spring Data发现unsafe不安全的排序方法时将跳过检查传入的排序字符串是否属于当前实体的属性,而是仅仅将sort子句添加到查询中。从而达到了LENGTH()方法生效的目的。

3.3 Native

@Query注解使用原生查询(即native=true)时,无法使用在参考中传入Sort的方式来达到排序的目的:

如果这样使用Sort:

    // @Query(value = "SELECT * FROM USERS u WHERE u.status = 1", nativeQuery = true)
    // List<User> findAllUsersSortUsingNative(Sort sort);

则将触发InvalidJpaQueryMethodException异常。

org.springframework.data.jpa.repository.query.InvalidJpaQueryMethodException: Cannot use native queries with dynamic sorting and/or pagination

无法将Native原生查询与动态排序和/或分页一起使用

控制台中的异常消息提示我们:JPA无法在原生的查询中动态地处理排序或是分页。

4.分页

分页功能允许我们仅在Page中返回所有数据的一部分。例如,在浏览网页上的几页数据时,当用户浏览到第一页时,服务器只需要返回第一页的数据即可。

分页的另一个优点是:相较于返回所有数据,返回某一页的数据减小了服务器发送到客户端的数据量。在同等情况下,较少的数据量意味着较高的性能。

4.1 JPQL

在JPQL查询定义中使用分页很简单:

@Query(value = "SELECT u FROM User u ORDER BY id")
Page<User> findAllUsersWithPaginationUsingJPQL(Pageable pageable);

Pageable是一个接口,而PageRequest是它的一个实现类。我们可以传递PageRequest参数来获取数据页面。原生查询也支持分页,但是需要一些额外的调整。

4.2 原生Native查询

我们可以通过声明附加属性countQuery启用原生查询的分页功能,该属性的主要作用是获取符合当前查询条件的总数,也更生成Page分页的具体信息(总共多少页,共多少条,是否为首页等):

@Query(
  value = "SELECT * FROM Users ORDER BY id", 
  countQuery = "SELECT count(*) FROM Users", 
  nativeQuery = true)
Page<User> findAllUsersWithPaginationUsingNative(Pageable pageable);

4.3 2.0.4之前的Spring Data JPA版本

针对原生查询的方法在Spring Data JPA 2.0.4及更高版本中均能正常工作。

在该版本之前,当执行此类查询时,我们将收到一个与上一节有关排序的内容相同的异常。

我们可以通过在查询中添加一个用于分页的附加参数,来克服这一问题:

@Query(
  value = "SELECT * FROM Users ORDER BY id \n-- #pageable\n",
  countQuery = "SELECT count(*) FROM Users",
  nativeQuery = true)
Page<User> findAllUsersWithPaginationUsingNativeBeforeJPA2_0_4(Pageable pageable);

在上面的示例中,我们添加“ \ n– #pageable \ n”作为分页参数的占位符。这告诉Spring Data JPA如何解析查询并注入pageable参数。

我们已经介绍了如何通过JPQL和原生SQL创建简单的选择查询。接下来,我们将展示如何定义其他参数。

5.索引参数查询

我们可以通过两种方法将方法参数传递给查询。在本节中,在本节中我们将介绍索引参数,下一节将介绍命名参数

索引参数命名参数本质是一样的。它们的不同点在于是否需要定义查询参数的名称以及是否需要注意参数的顺序:

  • 索引参数的模式下: 不需要定义查询条件的名称,但要保证查询条件的顺序和参数的顺序相同
  • 在命名参数的模式下:需要手动设置参数的名称,但不用考虑参数出现的顺序。

5.1 JPQL

对于JPQL中的索引参数,Spring Data会将方法参数按照在方法声明中出现的顺序传递给查询:

@Query("SELECT u FROM User u WHERE u.status = ?1")
User findUserByStatusUsingJPQL(Integer status);
 
@Query("SELECT u FROM User u WHERE u.status = ?1 and u.name = ?2")
User findUserByStatusAndNameUsingJPQL(Integer status, String name);

对于上述查询,status参数将被分配给查询参数?1,name参数将被分配给查询参数?2。方法中参数出现的顺序,和SQL中查询条件的顺序相对应。

5.2 Native

原生查询的索引参数与JPQL的工作方式完全相同:

@Query(
  value = "SELECT * FROM Users u WHERE u.status = ?1", 
  nativeQuery = true)
User findUserByStatusUsingNative(Integer status);

6.命名参数查询

我们还可以使用命名参数将方法来传递查询条件。使用@Param注解定义这些参数。

@Param注解中的参数字符串,必须与相应的JPQL或SQL查询参数名称匹配。与索引参数相比,使用命名参数易于阅读,并且在需要重构查询的情况下更不易出错

6.1 JPQL

如上所述,我们在方法声明中使用@Param注解,将JPQL中的参数与方法中的参数进行匹配:

@Query("SELECT u FROM User u WHERE u.status = :status and u.name = :name")
User findUserByStatusAndNameNamedParamsUsingJPQL(
  @Param("status") Integer status, 
  @Param("name") String name);

请注意,在上面的示例中,只要 Param中的字符串 和 JPQL中的查询条件 对应即可,不需要 传入的变量 和 查询条件 对应。所以以下在@Query中调换了name与status的写法也是完全正确的:

@Query("SELECT u FROM User u WHERE u.name = :name and u.status = :status")
User findUserByStatusAndNameNamedParamsUsingJPQL(
  @Param("status") Integer status, 
  @Param("name") String name);

6.2 Native

对于原生查询定义,与JPQL相比没有什么不同。同样是使用@Param注解,只不过语句换成了SQL:

@Query(value = "SELECT * FROM Users u WHERE u.status = :status and u.name = :name",
nativeQuery = true)
User findUserByStatusAndNameNamedParamsUsingNative(
@Param("status") Integer status, @Param("name") String name);

7.集合参数查询

让我们看下如何处理JPQL或SQL查询中若包含  IN(或NOT IN)关键字的情况:

SELECT u FROM User u WHERE u.name IN :names

上面JPQL的含义是:传入一个姓名集合,只要一个用户的姓名被包含在这个集合中,这条记录就会被查出来。在这种情况下,我们可以定义一个以Collection 为参数的查询方法  :

@Query(value = "SELECT u FROM User u WHERE u.name IN :names")
List<User> findUserByNameListUsingJPQL(@Param("names") Collection<String> names);

由于参数是Collection是集合接口,而所有的列表都继承自这个接口,因此可以与List,HashSet等一起使用。接下来,我们将展示如何使用@Modifying注解修改数据。

8.使用@Modifying执行更新操作

我们还可以通过将@Modifying注解添加到仓库层的方法上,并结合@Query注解中的语句,来完成对数据库进行更新操作。

8.1 JPQL

select查询相比,update方法有两个区别:一是它具有@Modifying注解,二是查询使用update关键字,而不是select

@Modifying
@Query("update User u set u.status = :status where u.name = :name")
int updateUserSetStatusForNameUsingJPQL(@Param("status") Integer status, 
  @Param("name") String name);

索引参数和命名参数都可以在更新语句中使用。此外如果声明了返回值,则返回值中会给出“执行相关的方法后具体更新了多少行记录”。

8.2 Native

我们还可以在原生查询上使用@Modifying注解以达到修改数据的目的:

@Modifying
@Query(value = "update User u set u.status = ? where u.name = ?", nativeQuery = true)
int updateUserSetStatusForNameUsingNative(Integer status, String name);

8.3 Inserts 执行插入操作

由于INSERT并不是JPA接口的一部分,所以执行插入操作我们必须同时应用@Modifying并使用原生查询

@Modifying
@Query(value = "insert into User (name, status, email) values (:name, :status, :email)", nativeQuery = true)
void insertUserUsingNative(@Param("name") String name, @Param("status") Integer status, @Param("email") String email);

9.动态查询

通常我们会遇到这种情况:软件运行前无法确定具体的SQL语句,而只有运行时才知道某些参数的值,近而再根据这些值来构建不同的SQL语句。在这种情况下静态查询便不能胜任了,此时便需要借助于Criteria来实现动态查询。

9.1 动态查询的例子

假设我们需要查询电子邮件包含在一组列表中的用户,而此电子邮件列表是动态生成(不确定)的:

SELECT u FROM User u WHERE u.email LIKE '%email1%' 
    or  u.email LIKE '%email2%'
    ... 
    or  u.email LIKE '%emailn%'

由于电子邮件列表是动态构造的,因此在编译时我们无法知道要添加多少个LIKE子句。

那么这种情况下,我们使用@Query注解便无法完成功能需求了,因为我们无法提供静态SQL语句

此时我们可以通过实现定制一个动态的查询方法,并在该方法中根据当前的实际逻辑来动态的生成查询语句。最后,为了让其能够与Spring Data JPA完结合,再按一定的规范来组织接口、类与方法,从而达到自定义动态查询方法的目的。

9.2 自定义仓库与JPA Criteria API

Spring Data 创建自定义可组合仓库一文详细的表达了如果自定义仓库来优雅地完成动态查询。基本步骤如下:

创建自定义的接口,并在接口中定义查询方法、参数:

public interface UserRepositoryCustom {
    List<User> findUserByEmailsUsingCriteria(Set<String> emails);
}

接下来,新建一个上述接口的实现类,该类的名称必须为 接口名+Impl:

public class UserRepositoryCustomImpl implements UserRepositoryCustom {
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<User> findUserByEmailsUsingCriteria(Set<String> emails) {

        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> user = query.from(User.class);

        Path<String> emailPath = user.get("email");

        List<Predicate> predicates = new ArrayList<>();
        for (String email : emails) {
            predicates.add(cb.like(emailPath, email));
        }
        query.select(user)
                .where(cb.or(predicates.toArray(new Predicate[predicates.size()])));

        return entityManager.createQuery(query)
                .getResultList();
    }
}

如上所示,我们利用了JPA Criteria API在findUserByEmailsUsingCriteria中构建了动态查询。

另外,我们需要确保在类名为:接口名+Impl因为只有这样Spring才会认为UserRepositoryCustomImpl是UserRepositoryCustom在仓库层面的实现类。Spring依靠此接口名+Impl机制来查找仓库的实现类。如果名称不对应,Spring会把findUserByEmailsUsingCriteria接口当成仓库的内部方法,并尝试对其进行解析,在这种情况下Sring会根据方法来尝试User实体emailsUsingCriteria字段,由于User实体并不存在该字段,所以最终会触发“EmailsUsingCriteria属性不存在”的异常。

9.3 扩展现有仓库层

我们通过继承JpaRepository仓库的同时继承自定义UserRepositoryCustom来达到扩展UserRepository的目的。

public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
    // 第2-7节中的代码
}

此时,当调用Spring自动生成的UserRepository对象的findUserByEmailsUsingCriteria方法时,将是终调用UserRepositoryCustomImpl.findUserByEmailsUsingCriteria()方法。

9.4 使用仓库层

最后,我们可以如下调用动态查询方法:

Set<String> emails = new HashSet<>();
// 在集合中添加一些邮箱...
emails.add("test@test.com");
List<User> users = userRepository.findUserByEmailsUsingCriteria(emails);

10.总结

在本文中,我们介绍了使用@Query注解在Spring Data JPA仓库方法中定义查询的几种方法。包括使用JPQL、使用原生SQL、以及如何自定义仓库层,并使用Criteria实现动态查询。

分类
Spring Data

Spring Data JPA 简介

1. 概述

本文将围绕Spring项目中的Spring Data JPA展开介绍。如果你尚需要了解如何使用Spring来构造一个基本的应用程序,那么可以先阅读此篇文章。Spring JPA是一种新颖、优雅的数据访问方式,它允许我们在只定义接口及方法名称的前提下,快速的实现对数据库的访问。

2. Spring Data中的DAO

DAO模式:Data Access Object数据访问(存储)对象模式,简单来讲就是使用JAVA语言来快速、简单的操作数据库。

该模式一般由以下部分组成:

DAO接口:将对数据库的操作定义为抽象方法,比如定义save方来新增数据。

DAO实现类:负责DAO接口中抽象方法的具体实现。往往会根据数据库类型而给出不同的实现。

实体类:用于与数据库中的数据表进行映射。DAO可以将实体对象操作到数据表中,也可以将查询出的数据绑定到实体上。

基础类:为一些样板代码提供一些基础的支持,避免写过多的样板化代码。

https://www.runoob.com/note/27029

使用Spring简化DAO层的样板代码一文中,我们阐述了DAO层通常包含了过多重复的样板化的代码。对样板代码简单可以减少代码体量,降低维护与升级的成本,统一数据访问方式、配置信息等。

Spring Data对样板代码的简化做到了极致,在Spring Data中我们只需要定义相关接口即可,完全不需要对该接口进行实现。

若使定义的DAO接口生效,则需要使其继承JPA中指定的仓库接口 -- JpaRepository。Spring Data将会自动扫描继承了JpaRepository的接口并自动为其创建一个实现。

通过继承JpaRepository的方法,我们可以轻松的获取到一个包含了CRUD操作的标准DAO。

3. 自定义方法以及查询

虽然能过继承JpaRepository已经实现了基本的CRUD操作,但大多数的项目中,仅仅有CRUD操作是远远不够的。

为此,Spring JPA提供了多种自定义数据操作的方法:

  • 直接在接口中定义一个新的方法,并使用支持JPQL的@Query注解来进行标识
  • 使用高级用法:SpecificationQuerydsl
  • 通过JPA命名查询(Named Queries)来自定义查询方法

第二种方法与JPA的标准查询比较相似,不同的是使用这种方法将更加灵活、方便。这将使我们的代码具有更高的可读性,可复用性也会更强。特别是当我们处理一些复杂的查询逻辑时,这种方法的优势将更加突出。

3.1 仅需要定义方法名

当Spring Data扫描到继承了JpaRepository的接口并生成实现时,Spring Data将扫描接口定义的方法并尝试依据方法名将其自动转换为特定的查询语句。尽管说这种依据方法名来自动生成查询语句的方法有一定的局限性,但这种方法使用起来真的是太强大、太方便、太优雅了。

假设当前有学生(student)实体,我们此时想以学生姓名做为关键字进行查询,则仅仅需要在定义的DAO接口中加入以下方法:

/**
 * 操作学生表的DAO
 *
 * @author panjie
 */
public interface StudentDAO extends JpaRepository<Student, Long> {
    /**
     * 通过姓名查找某个学生
     *
     * @param name 学生姓名
     * @return
     */
    Student findByName(String name);
}

没错,仅仅需要定义一个方法,Spring Data将自动按此方法名称转换为:根据关键字来查询某个学生。这种查询方法支持很多种关键字,如果在不需要对逻辑进行处理时,不失为最佳的一种方法。更多的关键字请参考官方文档

当然了,在使用上述方法时,需要保证实体中的字段信息与方法中给出的信息一致,以避免发生异常。比如findByUsername方法的成功执行依赖于学生实体(student)中存在username字段,而此时学生实体并不存在userName字段,则解析器将抛出如下异常:

club.codedemo.thepersistencelayerwithspringdatajpa.StudentDAO.findByUsername(java.lang.String)! No property username found for type Student!

3.2 自定义查询语句

可以在方法上添加@Query注解以自定义查询语句:

    /**
     * 不区分大小写的根据name查询某个学生
     * 比如通过Zhangsan可以查询出学生名为zhangsan、zhangSan等的学生
     *
     * @param name 学生姓名
     * @return
     */
    @Query("SELECT s FROM Student s WHERE LOWER(s.name) = LOWER(:name)")
    Student retrieveByName(@Param("name") String name);

欲了解更多关于自定义查询语句的知识,请参考官方文档

4. 事务配置

Spring自动实现接口的特性决定了我们无法直接获取到整个实现过程,当然也无法得知Spring Data JPA是如何对事务进行配置的了。值得庆幸的事,我们可以通过观察Spring Data JPA中的org.springframework.data.jpa.repository.support.SimpleJpaRepository类了解其事务配置。

Spring使用了@Transaction(readOnly = true)对该类进行注解,表示该类中的方法默认采用的都是只读(read-only)模式。接着又在个别的非查询模式的方法上加入了@Transaction,从而覆盖了在类上标注的只读模式,近而达到了:如果该类中的某个方法上没有使用@Transaction注解,则标识在该类上的@Transaction(readOnly = true)起作用;如果某个方法使用了@Transaction注解,则忽略类上的@Transaction(readOnly = true)注解。

4.1 异常转换依然有效

现在可能你有一个疑问:既然Srping Data JPA并不依赖于已经在Spring5中移除的历史的模板引擎JpaTemplate与HibernateTemplate,那么我们是否仍要将JPA异常转换为Spring的DataAccessException呢?

答案是肯定的,我们仍然可以在DAO层使用@Repository注解来开启异常转换功能。@Repository注解将自动获取bean中的PersistenceExceptionTranslator并将其转换为我们熟知的DataAccessException

比如执行以下代码最终将获取DataIntegrityViolationException(该异常是DataAccessException的子类):

    /**
     * 由于Student实体中要求name字段不能为null
     * 所以保存一个name字段为null的默认学生实体时,将会发生DataIntegrityViolationException异常
     */
    @Test
    void givenStudentHaveNoName() {
        Student student = new Student();
        Assertions.assertThrows(DataIntegrityViolationException.class,
                () -> this.studentDAO.save(student));
    }

需要时刻提醒自己的是:和很多注解一样,异常转换是通过代码模式完成的。所以相关的方法绝对不能够声明为final

5. Spring Data JPA仓库配置

使用@EnableJpaRepositories来启用Spring JPA的仓库支持(自动根据接口创建DAO实现),同时在该注解中需要同时指定扫描的基础包:

/**
 * 测试EnableJpaRepositories注解时,请 注释/启用 11,12,13行后
 * 分别查看单元测试EnableJpaRepositoriesTest中获取bean的效果
 */
@SpringBootApplication
@EnableJpaRepositories(
		basePackages = "club.codedemo.repository"
)
public class ThePersistenceLayerWithSpringDataJpaApplication {

使用XML的话,配置如下:

<jpa:repositories base-package="com.baeldung.spring.data.persistence.repository" />

6. JAVA或XML配置

在认识Spring JPA一文中已经对如何在Spring中配置JPA进行了深入了讨论。除了在前文中讨论的内容以外,如果说我们使用XML进行配置,则需要在@ImportResource中指定XML的位置:

@Configuration
@EnableTransactionManagement
@ImportResource("classpath*:*springDataConfig.xml")
public class PersistenceJPAConfig {
    ...
}

7. MAVEN依赖

就像前文中提及的一样,使用JPA还需要加入spring-data-jpa依赖:

<dependency>
   <groupId>org.springframework.data</groupId>
   <artifactId>spring-data-jpa</artifactId>
   <version>2.2.7.RELEASE</version>
</dependency>

8. 使用Spring Boot

我们还可以使用Spring Boot Starter Data JPA依赖,其将会自动的为项目配置数据源。

当然了,自动配置数据源的前提是需要让其能够检测到当前项目使用的数据源,如果在项目中并没有提供任何数据源,则将发生一个异常。比如我们在项目中添加H2内容数据库:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
   <version>2.2.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.200</version>
</dependency>

则项目在启动时,将自动加载H2数据库做为数据源。

在未进行任何配置的情况下,Spring Boot在启动时,将会按标准的默认配置进行加载。这些默认的配置项可以轻松的通过application.properties文件进行更改:

spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa

比如我们通过以代码来修改数据源的地址以及数据库的认证信息。

9. 总结

本文对Spring 5、JPA2、Sptring Data JPA中数据持久化层的配置及实现进行介绍。在介绍的过程中给出了基于xml以及基于java的配置示例。

本文还讨论了自定义高级查询的几种方法、事务机制、异常转换以及如何扫描一个项目外的jpa命名空间。

总之Spring JPA是一种新颖、优雅的数据访问方式,它允许我们在只定义接口及方法名称的前提下,快速的实现对数据库的访问。

分类
Spring Data

Spring Data JPA 条件查询

1. 简介

Spring Data JPA中提供了多种数据查询的方式,比如说定义查询方法或使用自定义的JPQL。但有些时候,上述查询方式并不能够很好的满足我们对复杂综合查询的需要,这时候就需要Criteria APIQueryDSL登场了。

Criteria API提供了一种使用代码创建动态查询语句的功能,相较于编写传统的SQL语句,该方法能够有效的避免一些语法错误。当结合Metamodel API使用时,甚至可以在编译的阶段发现一些字段或类型的错误。

当然,凡是都有两面性,它的缺点是:即使实现一个看似不起眼的查询方法,也需要编写长篇的样板(只所以称为样板是由于每次写的都差不多)代码,这使得阅读代码的时候不是很爽;再者由于需要接受一些新的知识,所以对于初次接触Criteria API的小伙伴们并不友好。

本文将讨论如何使用criteria queries(条件查询)来实现自定义的数据访问逻辑,以及Spring是如果帮助我们减少样板代码的。

2. 需求假设

假设如下图书(Book)实体,我们需要实现的功能是:查询某个作者(author)并且图书名称包含特定的关键字的所有图书。

/**
 * 图书
 */
@Entity
public class Book {
    @Id
    @GeneratedValue
    private Long id;

    /**
     * 名称
     */
    String title;

    /**
     * 作者
     */
    String author;
    // setter and getter
}

本着一事一议、尽量减少干扰项的原则,本文引入Metamodel API。

3. 数据仓库类

按Spring规范,我们使用@Repository 来标识自定义数据仓库。要实现Criteria API还需要在数据仓库中注入EntityManager实例:

/**
 * 图书数据访问对象
 */
@Repository
public class BookDao {

    /**
     * 实体管理器
     */
    @Autowired
    EntityManager em;

    /**
     * 查询图书
     * @param authorName 作者名称(精确查询)
     * @param title 图书名称 模糊查询
     * @return
     */
    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Book> cq = cb.createQuery(Book.class);

        Root<Book> book = cq.from(Book.class);
        Predicate authorNamePredicate = cb.equal(book.get("author"), authorName);
        Predicate titlePredicate = cb.like(book.get("title"), "%" + title + "%");
        cq.where(authorNamePredicate, titlePredicate);

        TypedQuery<Book> query = em.createQuery(cq);
        return query.getResultList();
    }
}

上述代码遵循了标准的Criteria API 工作流:

  • 首先获取了一个CriteriaBuilderCriteriaBuilder是整个查询功能的基础。
  • 使用CriteriaBuilder创建了一个CriteriaQuery<Book>,泛型中的Book指明了我们想获取的数据是Book实体。
  • 使用CriteriaQuery<Book>的from方法来获取了一个Root<Book> book,指定了此次查询的起点为Book实体(对应book) 数据表。
  • 接下来使用CriteriaBuilder创建了两个基于Book实体的查询条件(仅仅是创建了一个查询条件,只有将其放入正式的查询中,该查询条件才会生效)。查询条件一用于查找book表中的author字段与传入authorname完全相同的记录;条询条件二用于查找book表的title字段左右模糊匹配title字段。
  • cp.where将两个查询条件组合到一起,表示:查询满足查询条件一并且同时满足查询条件二的数据。
  • 然后使用CriteriaQuery创建了一个TypedQuery<Book>实例,该实例具备了数据查询的能力。
  • 最后调用了TypedQuery的getResultList()方法,此时将综合查询条件进行数据查询,并且将获取的数据依次填充到CriteriaQuery<Book>上规定的<Book>实体上。

值得提出的一点时,由于我们在当前数据访问对象BookDao使用@Repository注解,所以spring将默认托管该类所产生的异常。

4. 使用自定义的查询的方法扩展数据仓库层

虽然我们可以使用Spring Data中根据方法名称自动创建动态查询的功能来完成一些个性化的查询工作。但当有一些些逻辑处理参杂在查询过程中时(比如对null值进行过滤),这种自动创建查询方法的功能便力不从心了。

当然了,我们完全可以像上一节展示的一样来创建自定义的数据仓库来实现一些复要的查询功能。

但我想说的是,参考Spring Data组合数据仓库一文将一些自定义的方法添加到JPA的@Repository接口机制,使用继承接口的方法来完成数据创建的初始化,会有一种更好开发体验。

比如我们创建如下BookRepositoryCustom接口:

/**
 * 自定义的Book数据仓库
 */
public interface BookRepositoryCustom {
    /**
     * 查询图书
     * @param authorName 作者名(精确)
     * @param title 图书名称(模糊)
     * @return
     */
    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title);
}

然后在创建的数据仓库接口中继承BookRepositoryCustom:

interface BookRepository extends JpaRepository<Book, Long>, BookRepositoryCustom {}

值得注意的是,如果想使用BookDao中的findBooksByAuthorNameAndTitle方法做为BookRepositoryCustom中方法的实现,则必须将BookDao重命名为BookRepositoryImpl,同时实现BookRepositoryCustom接口。

在此,我们保持原BookDao名称不变。并新建BookRepositoryImpl:

/**
 * 图书数据访问对象
 */
@Repository
public class BookRepositoryImpl implements BookRepositoryCustom {

    /**
     * 实体管理器
     */
    @Autowired
    EntityManager em;

    /**
     * 查询图书
     * @param authorName 作者名称(精确查询)
     * @param title 图书名称 模糊查询
     * @return
     */
    @Override
    public List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
    }
}

然后使用另外一种方法来实现查询的功能,该功能将对排除掉值为null的传入参数:

@Override
List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Book> cq = cb.createQuery(Book.class);
 
    Root<Book> book = cq.from(Book.class);
    List<Predicate> predicates = new ArrayList<>();
    
    if (authorName != null) {
        predicates.add(cb.equal(book.get("author"), authorName));
    }
    if (title != null) {
        predicates.add(cb.like(book.get("title"), "%" + title + "%"));
    }
    cq.where(predicates.toArray(new Predicate[0]));
 
    return em.createQuery(cq).getResultList();
}

其实无论是哪种方法,都可以发现同一个特点:代码像懒婆娘的裹脚布一样,又臭又长。

接下来,我们继续展示使用JPA Specifications来掀开这又臭又长的裹脚布。

5. JPA Specifications

Spring Data 提供了org.springframework.data.jpa.domain.Specification来简化查询操作时的样板代码:

interface Specification<T> {
    Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb);
}

该接口提供了toPredicate方法,在方法中注入了样板代码中需要的root、CriteriaQuery以及CriteriaBuilder。我们可以利用该接口来快速的创建查询条件:

/**
 * 查询作者
 * @param author 作者
 * @return
 */
static Specification<Book> hasAuthor(String author) {
    return (book, cq, cb) -> cb.equal(book.get("author"), author);
}
 
/**
 * 查询图书名
 * @param title 名称
 * @return
 */
static Specification<Book> titleContains(String title) {
    return (book, cq, cb) -> cb.like(book.get("title"), "%" + title + "%");
}

在使用上述查询条件以前,我们还需要在数据仓库上继承org.springframework.data.jpa.repository.JpaSpecificationExecutor<T>

interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificationExecutor<Book> {}

此时,当前数据仓库层便能够处理Spring Data的Specification查询条件了。此时,如若获取某个作者的所有图书,则可以简短的这样写:

bookRepository.findAll(hasAuthor(author));

美中不足的是,JpaSpecificationExecutor提供的findAll仅仅支持传入一个Specification查询条件。幸运的是我们可以使用org.springframework.data.jpa.domain.Specification接口提供的方法来完成查询条件的组合。

比如将作者与图书名两个查询条件组合在一起进行综合查询:

bookRepository.findAll(where(hasAuthor(author)).and(titleContains(title)));

上述代码中,我们使用了Specification接口上的where方法来完成了两个查询条件的组合。

这样以来,得益于伟大的Spring,我们在实现了查询模块化(可以轻易的按某一个或多个条件进行组合查询)的同时,还让代码看起来没有那么臃肿。

当然了,这并不意味着我们构造综合查询时可以完全的脱离样板化的代码。在有些时候,合适样板化代码来完成一些特定的查询功能也是非常有必要的。比如我们在查询中若想使用group进行分组,或是查询A数据表后,将数据填充到B实体上,再或者进行一些子查询。

条条大路通北京。综合查询的实现也是一样,没有最好的方法,只有最合适的方法。在实际的使用过程中,应该根据当前的业务需求,尝试选择出最合适的那个查询方式。

6. 总结

本文中先后介绍了3种不同的综合查询方法:

  • 最简单、直接的创建自定义的DAO
  • @Repository进行扩展,就像访问标准的查询方法一样调用综合查询方法
  • 使用Specification减少样板化的代码

同样,你可以打开文章顶部的github地址来获取一份与本文相同的且可运行的code demo。不止如此,在code demo中我们还提供了用于验证的单元测试代码。希望能对你有所帮助。

分类
persistence

Criteria API IN表达式使用示例

1. 概述

我们在进行查询时,经常会遇到查询实体的某个字段位于某个范围的情况。

本文我们将介绍如何使用Criteria API来解决此类问题。

2. 示例实体

在正式开始以前,我们先准备如下两个实体:Department以及DeptEmployee,这两个实体间的关系为 1:n

DeptEmployee实体如下,该实体中使用@ManyToOne注解声明了多对一的关系。

@Entity
public class DeptEmployee {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;
 
    private String title;
 
    @ManyToOne
    private Department department;
    
    // 以下省略了setter/getter
}

Department 实体如下,使用@OneToMany注解声明了一对多的关系:

@Entity
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;
 
    private String name;
 
    @OneToMany(mappedBy="department")
    private List<DeptEmployee> employees;

    // 以下省略了setter/getter
}

3. CriteriaBuilder.In

首先让我们看一下CriteriaBuilder接口。该接口上定义了一个in()方法,此方法接收的参数类型为 Expression,返回值类型为Predicate。将返回的Predicate做为查询条件传入至where()方法中,即可将范围做为某个字段的查询条件:

CriteriaQuery<DeptEmployee> criteriaQuery = 
  criteriaBuilder.createQuery(DeptEmployee.class);
Root<DeptEmployee> root = criteriaQuery.from(DeptEmployee.class);
In<String> inClause = criteriaBuilder.in(root.get("title"));
for (String title : titles) {
    inClause.value(title);
}
criteriaQuery.select(root).where(inClause);

在Spring Boot中,你可以如下获取criteriaBuilder(GITHUB示例代码):

    @Autowired
    EntityManager entityManager;
    ...
    CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();

或者配合仓库层如下使用(GITHUB示例代码):

    @Test
    void spring() {
        String[] titles = {title};
        Specification<DeptEmployee> deptEmployeeSpecification = new Specification<DeptEmployee>() {
            @Override
            public Predicate toPredicate(Root<DeptEmployee> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {

                CriteriaBuilder.In<String> inClause = criteriaBuilder.in(root.get("title"));
                for (String title : titles) {
                    inClause.value(title);
                }
               return inClause;
            }
        };


        assertEquals(10,  this.deptEmployeeCrudRepository.findAll(deptEmployeeSpecification).size());
    }

4. Expression.In

另外,还可以使用Expression接口提供的in()方法:

criteriaQuery.select(root)
  .where(root.get("title")
  .in(titles));

CriteriaBuilder.in()不同的是,Expression.in() 接收的参数类型为Collection。对比上述两处代码可以看出Expression.in() 的写法更简单一些。

5. 在子查询中使用IN表达式

接下来让我们看看如何在子查询中使用IN表达式。

比如有如下需求:查询出所有部门名称中包含某个搜索值的所有部门Department的所有员工DeptEmployee

        Subquery<Department> subquery = criteriaQuery.subquery(Department.class);
        Root<Department> dept = subquery.from(Department.class);
        subquery.select(dept)
                .distinct(true)
                .where(criteriaBuilder.like(dept.get("name"), "%" + departmentName + "%"));

        criteriaQuery.select(employeeRoot)
                                                            .where(criteriaBuilder.in(employeeRoot.get("department")).value(subquery));

如上代码:我们创建一个名为subquery的子查询,并将其做为一个表达式传入了value()方法中。

6. 总结

本文中,我们介绍了几种IN查询的方法,在文章的最后对子查询给出了示例。希望对你能有所帮助。

分类
Series Spring Persistence

Spring 持久化(操作数据库)系列教程