分类
Data Jackson Json

忽略Jackson的空字段

译者注

原文:

https://www.baeldung.com/jackson-ignore-null-fields

官方Demo:

https://github.com/eugenp/tutorials/tree/master/jackson-simple

1. 前言

在本文中我们将会学习,在序列化一个Java类时,如何通过设置Jackson,来实现忽略空字段

如果想要获得更深入了解Jackson的其他好玩的用法,请点击Jackson基本教程

2. 在类中忽略空字段

我们可以使用Jackson在对象的级别上来控制空字段,此时只需要在类上添加注解:

@JsonInclude(Include.NON_NULL)
public class MyDto { ... }

或者在字段层面更细微的控制空字段,这种用法需要把注解加在字段上:

public class MyDto {

    @JsonInclude(Include.NON_NULL)
    private String stringValue;

    private int intValue;

    // standard getters and setters
}

现在我们进行测试就会发现,空值确实没有出现在JSON输出中:

@Test
public void givenNullsIgnoredOnClass_whenWritingObjectWithNullField_thenIgnored()
  throws JsonProcessingException {
    ObjectMapper mapper = new ObjectMapper();
    MyDto dtoObject = new MyDto();

    String dtoAsString = mapper.writeValueAsString(dtoObject);

    assertThat(dtoAsString, containsString("intValue"));
    assertThat(dtoAsString, not(containsString("stringValue")));
}

3. 全局忽略空字段

Jackson还可以通过对象映射表(ObjectMapper)来全局的配置空字段。

mapper.setSerializationInclusion(Include.NON_NULL);

经过以上的配置后,任何对象在通过这个映射表进行序列化时,都会忽略空字段:

@Test
public void givenNullsIgnoredGlobally_whenWritingObjectWithNullField_thenIgnored() 
  throws JsonProcessingException {
    ObjectMapper mapper = new ObjectMapper();
    mapper.setSerializationInclusion(Include.NON_NULL);
    MyDto dtoObject = new MyDto();

    String dtoAsString = mapper.writeValueAsString(dtoObject);

    assertThat(dtoAsString, containsString("intValue"));
    assertThat(dtoAsString, containsString("booleanValue"));
    assertThat(dtoAsString, not(containsString("stringValue")));
}

4. 结论

“忽略空字段”是Jackson常见的一个基本配置,通常我们需要更好地控制JSON输出,本文演示了如何通过它来控制类。
此外,还有更多的高级用法,例如“当序列化一个映射时忽略空字段

Github上可以找到本文的示例代码。

分类
Java

Java内存不足错误: 垃圾回收超出开销限制

译者注

原文: https://www.baeldung.com/java-gc-overhead-limit-exceeded
Demo: https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-perf

1. 概述

简单来说,当应用中的对象不再被使用时,JVM便会进行回收内存的操作,我们把这个操作称为垃圾回收,对应的原文为Garbage Collection,简称GC

GC Overhead Limit Exceeded error提示是由java.lang.OutOfMemoryError发出的,此异常代表资源已经被耗尽。

本文中我们将介绍引发java.lang.OutOfMemoryError: GC Overhead Limit Exceeded error的原因及解决方案。

2. “垃圾回收超出开销限制”的原因(GC Overhead Limit Exceeded Error)

OutOfMemoryError类是java.lang.VirtualMachineError的一个子类,当JVM 遇到资源相关的问题时,就会抛出此VirtualMachineError异常。
具体来讲,当JVM花费了太多的时间去执行垃圾回收,却只回收了很少的可用堆空间时,这个错误就会出现。

根据Java的文档, 在默认情况下JVM的设计是:如果Java进程花费了超过98%的时间去执行垃圾回收,而每次只收回了不到2%的内存时,就会抛出这个错误。
换句话讲,这就意味着我们的程序耗尽了几乎所有的可用内存,并且GC已经花费了太多了时间去清理内存,却不断的失败,不断的重复。

在这种情况下,这个应用程序的用户体验会变得非常缓慢。那些本来只需要花费毫秒级时间就能完成的操作,现在却要花费更多的时间才能完成。这是因为CPU正在使用它的计算资源来执行GC操作,因此无法执行任何其他的任务。

3. 复现这个问题

我们看一下抛出java.lang.OutOfMemoryError: GC Overhead Limit Exceeded错误的代码。

我们可以知道,例如,通过添加键值对在一个不会结束的循环(死循环)中:

public class OutOfMemoryGCLimitExceed {
    public static void addRandomDataToMap() {
        Map dataMap = new HashMap<>();
        Random r = new Random();
        while (true) {
            dataMap.put(r.nextInt(), String.valueOf(r.nextInt()));
        }
    }
}

当这个方法被调用的时候,带着如下的JVM参数运行`-Xmx100m -XX:+UseParallelGC` (这个参数的含义是:设置Java的堆大小为100M,GC收集算法为并行收集),我们就会得到一个`java.lang.OutOfMemoryError: GC Overhead Limit Exceeded`错误。
如果想要对于不同的垃圾回收策略有更深入的理解,可以学习Oracle的文章:[ Java Garbage Collection Basics tutorial.](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html " Java Garbage Collection Basics tutorial")

通过在项目的根目录运行下面的命令,我们马上就可以得到一个`java.lang.OutOfMemoryError: GC Overhead Limit Exceeded`错误:

mvn exec:exec

此外,值得注意的是,有些情况下,我们在遇到`GC Overhead Limit Exceeded`错误之前,先遇到了一个堆空间错误(`heap space error`)。

4. 解决“GC超出开销限制”错误

理想的办法就是,通过检查代码中的内存泄漏,去查找软件中的潜在问题、

下面的问题值得被解决:

  • 软件中的哪些对象占用了堆中的大量内存?
  • 这些对象在代码中的哪个位置被创建

我们也可以使用自动化图形工具(例如[JConsole](https://docs.oracle.com/en/java/javase/11/management/using-jconsole.html#GUID-77416B38-7F15-4E35-B3D1-34BFD88350B5 "JConsole")),来帮助我们检测代码中的性能问题,包括文中一直提到的java.lang.OutOfMemoryErrors。

最后一个办法是,通过改变JVM启动时的配置来增加堆内存。例如,给我们运行的Java程序设置1GB的堆内存:

java -Xmx1024m com.xyz.TheClassName

然而,这种做法并不能解决应用程序中存在的内存泄漏问题,而是会推迟这个问题的发生。因此,更可取的办法是,彻底重新评估应用程序的内存使用情况。

5. 结论

在本文中,我们学习了 java.lang.OutOfMemoryError: GC Overhead Limit Exceeded 错误,并且讨论了错误出现的原因以及如何解决它。
和其他文章一样,读者可以在Github中找到示例代码。

分类
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 上找到。

分类
Java

Java:Base64编码与解码

译者前言

原文地址:https://www.baeldung.com/java-base64-encode-and-decode
Demo仓库地址:https://github.com/liuyuxuan6666/java-base64-encode-demo

1.概述

在这个教程中,我们将会学习Java提供的Base64编码和解码功能。
我们主要讨论的是Java8中的API,顺便也会说一说Apache Commons包中的部分API。

2.Java8中Base64的使用

Java8通过java.util.Base64这个类,在标准API中实现了Base64编解码的相关功能。我们先从最基本的用法开始。

2.1. Java8中Base64的基本编码器(getEncoder)

“基本编码器”是最简单的编码方式,直接处理输入的字符,不进行任何分隔处理。

也就是把输入的字符按照某种确定的规则,映射到由"A-Z a-z 0-9 +/"这些字符组成字符集中,形成一个字符序列。

我们先来看简单的String字符串处理(代码文件见 demo_2_1):

// 设置需要编码的字符串
String originalInput = "test input";
// 直接使用Base64.getEncoder()方法编码
String encodedString = Base64.getEncoder().encodeToString(originalInput.getBytes());
// 将会输出 dGVzdCBpbnB1dA==

需要注意的是,此处我们直接使用基本的getEncoder()方法,来调用内部API,实现Base64的编码。

对应的解码,把getEncoder()替换为getDecoder()即可,这里注意,转换过程是先转换为字节流,再转换为字符串:

String encodedString = "dGVzdCBpbnB1dA==";
// 使用Base64.getDecoder()获得字节流  byte[10]: [116, 101, 115, 116, 32, 105, 110, 112, 117, 116]
byte[] decodedBytes = Base64.getDecoder().decode(encodedString);
// 新建一个String变量,把字节流转化成字符串
String decodedString = new String(decodedBytes);
// 将会输出 test input

译者注:如果想查看转换中的字节流是什么形式,打断点或加输出语句可以更好的帮助学习,注释中提供了字节流。

2.2. 无填充的Java8 Base64编码(withoutPadding)

在Base64编码中,输出的字符串长度必须是3的倍数。如果编码后长度不是3的倍数,编码器会在字符串的末尾添加1或2个字符来满足需要,这个字符是等号“=”(参考2.1,编码后的字符串中有两个等号)。
解码时,编码器先把这些等号去掉,再开始解码。
对于Base64填充的深入理解,可以参考Stack Overflow上的详细解释

但有的时候,我们不需要编码器自动为我们填充,例如某些情况下,只需要Base64编码而不需要解码,这个时候就可以不使用填充,而是直接进行编码。
使用方式也很简单,只需要在执行编码之前添加.withoutPadding()即可(demo_2_2):

String encodedString = Base64.getEncoder().withoutPadding().encodeToString(originalInput.getBytes());
// 将会输出 dGVzdCBpbnB1dA

与2.1对比,同样的字符串采用填充和无填充编码的唯一区别,就是去掉了末尾的等号。

2.3. Java8 URL编码(getUrlEncoder)

URL编码器和基本编码器非常相似,不同之处在于:

  1. 它使用URL和文件名安全的Base64字母表,也就是编码后的字符串符合文件命名规范
  2. 它默认不添加任何行分隔符

使用方法,只需要把基本编码器中的getEncoder替换为getUrlEncoder,如下(demo_2_3):

String originalUrl = "https://www.google.co.nz/?gfe_rd=cr&ei=dzbFV&gws_rd=ssl#q=java";
String encodedUrl = Base64.getUrlEncoder().encodeToString(originalUrl.getBytes());
// 将会输出 aHR0cHM6Ly93d3cuZ29vZ2xlLmNvLm56Lz9nZmVfcmQ9Y3ImZWk9ZHpiRlYmZ3dzX3JkPXNzbCNxPWphdmE=

解码也是相同的用法,这里使用getUrlDecoder,和前面普通字符串的解码相似,先转换为字节流,再转换为字符串:

byte[] decodedBytes = Base64.getUrlDecoder().decode(encodedUrl);
// byte[62]:
// [104, 116, 116, 112, 115, 58, 47, 47, 119, 119, 119, 46,
// 103, 111, 111, 103, 108, 101, 46, 99, 111, 46, 110, 122,
// 47, 63, 103, 102, 101, 95, 114, 100, 61, 99, 114, 38, 101,
// 105, 61, 100, 122, 98, 70, 86, 38, 103, 119, 115, 95, 114,
String decodedUrl = new String(decodedBytes);
// 将会输出 https://www.google.co.nz/?gfe_rd=cr&ei=dzbFV&gws_rd=ssl#q=java

2.4. Java8 MIME编码(getMimeEncoder)

什么是MIME?菜鸟教程中给出了详细解答:

MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的标准,用来表示文档、文件或字节流的性质和格式。
MIME 消息能包含文本、图像、音频、视频以及其他应用程序专用的数据。
浏览器通常使用 MIME 类型(而不是文件扩展名)来确定如何处理URL,因此 Web服务器在响应头中添加正确的 MIME 类型非常重要。如果配置不正确,浏览器可能会无法解析文件内容,网站将无法正常工作,并且下载的文件也会被错误处理。

我们先从基本的MIME输入开始编码(demo_2_4):

private static StringBuilder getMimeBuffer() {
    StringBuilder buffer = new StringBuilder();
    for (int count = 0; count < 10; ++count) {
        buffer.append(UUID.randomUUID().toString());
    }
    return buffer;
}

MIME编码器使用基本字母表生成Base64编码的结果,这个编码格式是对于MIME友好的。这体现在:

  1. 每行输出不超过76个字符
  2. 编码后的字符串以回车符和换行符结尾 (\r\n)
StringBuilder buffer = getMimeBuffer();
byte[] encodedAsBytes = buffer.toString().getBytes();
String encodedMime = Base64.getMimeEncoder().encodeToString(encodedAsBytes);

在解码过程中,我们可以使用getMimeDecoder()方法,仍然是字节流->字符串:

byte[] decodedBytes = Base64.getMimeDecoder().decode(encodedMime);
String decodedMime = new String(decodedBytes);

3. 使用Apache Commons Code包进行编码解码

前面提到的是用Java8内置的Base64 API来操作,此外,还有其他的第三方包也提供了Base64的一些功能,例如Apache Commons
假设我们使用的是Maven托管的项目,再pom.xml中添加commons-codec的依赖:

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.15</version>
</dependency>

译者注:如果你第一次接触maven,添加依赖后需要在项目根目录执行一次mvn install,或者在IDEA中选择pom.xml右键-Maven-重新加载项目,即可完成依赖的更新。

我们使用的是名为org.apache.commons.codec.binary.Base64的类,可以通过不同参数的构造方法来初始化一个实例对象:

  1. Base64(boolean urlSafe) —— 创建Base64对象,打开或关闭URL安全模式
  2. Base64(int lineLength) —— 创建Base64对象,不启用URL安全模式,通过参数控制行的长度,默认长度为76
  3. Base64(int lineLength, byte[] lineSeparator) —— 创建Base64对象,参数一控制行的长度,参数二设置额外的分隔符,默认为 CRLF(“\r\n”)

当Base64实例对象生成后,调用它进行编码解码就十分简单了。编码(demo_3):

// 输入需要编码的字符串
String originalInput = "test input";
// 用默认参数创建Base64对象
Base64 base64 = new Base64();
// 执行编码
String encodedString = new String(base64.encode(originalInput.getBytes()));
// 将会输出 dGVzdCBpbnB1dA==

此外,Base64类的decode()方法来进行解码,获得字符串:

String decodedString = new String(base64.decode(encodedString.getBytes()));
// 将会输出 test input

除了上面提到的使用Base64的实例对象的方式,还有一种方式不创建对象,而是直接用Base64类的静态方法:

String originalInput = "test input";
String encodedString = new String(Base64.encodeBase64(originalInput.getBytes()));
// dGVzdCBpbnB1dA==
String decodedString = new String(Base64.decodeBase64(encodedString.getBytes()));
// test input

4. 将字符串对象转化为字节数组

有些情况下,我们需要把String对象转化为byte[],有多种方法。
最简单的是直接使用String对象内置的getBytes()方法(demo_4):

String originalInput = "test input";
byte[] result = originalInput.getBytes();
assertEquals(originalInput.length(), result.length);

我们可以不使用默认的编码方式,而是为其指定一个:

String originalInput = "test input";
byte[] result = originalInput.getBytes(StandardCharsets.UTF_16);
assertTrue(originalInput.length() < result.length);

如果我们输入的是Base64编码后的字符串,想获得原始数据,可以使用Base64解码器:

String originalInput = "dGVzdCBpbnB1dA==";
byte[] result = Base64.getDecoder().decode(originalInput);
assertEquals("test input", new String(result));

我们还可以使用DatatypeConverter类的parseBase64Binary()方法:

String originalInput = "dGVzdCBpbnB1dA==";
byte[] result = DatatypeConverter.parseBase64Binary(originalInput);
assertEquals("test input", new String(result));

5. 总结

在本文中,我们学习了Java中如何使用Base64进行编码和解码。我们分别使用了Java8的原生API和ApacheCommons引入的新API。
除了文中提到的,还有其他相似功能的API,例如java.xml.bind.DataTypeConverter带有带有printHexBinary和parseBase64Binary功能。
要学习其他的API的使用方式,可以从GitHub上寻找更多的代码。

分类
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实现动态查询。