Spring Data JPA为Spring应用程序提供了数据访问层的实现。这是一个非常方便的组件,因为它不会重新发明每个新应用程序的数据访问方式,因此您可以花更多时间来实现业务逻辑。使用Spring Data JPA时有一些好的做法。例如,限制不必要的对象的加载以优化性能。

  本文将为您提供一些技巧,以减少请求数据库的次数,而不是检索数据库的所有元素,因此不会影响应用程序的整体性能。为此,我们首先会看到Spring Data JPA提供的各种工具来改进对数据访问的控制,以及一些良好的实践去减少数据检索对我们应用程序的影响的。然后,我将与您分享一个具体的例子,通过在这些不同的方面发挥作用来提高Spring应用程序的性能,从而减少潜在的问题。

实体关系的加载

  当使用Spring Data JPA创建应用程序(并且通常使用Hibernate)时,可能会自动加载对象依赖关系(例如书的作者)- EAGER;或手动加载 -  LAZY。

  使用EAGER类型依赖关系时,每次加载对象时,都会加载相关对象:当您询问书籍数据时,也会检索作者的数据。

  使用LAZY类型依赖关系时,只会加载所需对象的数据:不会检索作者的数据。

  使用Spring Data JPA,2个域对象之间的每个关系都拥有这些数据加载类型之一。默认情况下,该方法将由关系类型确定。

  以下是与其数据加载默认类型的所有可能关系:

@OneToOne

  对于实体A的每个实例,实体B的一个(并且仅有一个)实例被关联。B也仅与实体A的一个实例相关联。

  一个典型的例子是患者和他的记录之间的关系:

@Entity
public class Patient implements Serializable {
@OneToOne
private PatientRecord record;
}

  对于这种关系类型,默认的数据加载方法是EAGER:每次询问患者的数据时,患者记录的数据也将被检索。

@ManyToOne

  对于实体A的每个实例,有一个实体B(并且仅有一个)实例被关联。另一方面,B可能与A的许多实例相关联。

  一个典型的例子是产品及其类别之间的关系:

@Entity
public class Product implements Serializable {
@ManyToOne
private ProductCategory category;
}

  对于这种关系类型,默认数据加载方法是EAGER:每次询问产品数据时,该类别的数据也将被检索。

@OneToMany

  对于实体A的每个实例会关联实体B的零个、一个或多个实例。另一方面,B仅链接到A的一个实例。

  这与@ManyToOne关系相反,所以一个典型的例子可能是产品类别及其关联产品列表:

@Entity
public class ProductCategory implements Serializable {
@OneToMany
private Set<Product> products = new HashSet<>();
}

  对于这种关系类型,默认数据加载方法是LAZY:每次询问类别数据时,产品列表都不会被检索。

@ManyToMany

  对于实体A的每个实例会关联实体B的零个、一个或多个实例。相反的情况也是如此,B与零个、一个或多个A的实例相关联。

  一个典型的例子是博客文章与其主题列表之间的关系:

@Entity
public class Article implements Serializable {
@ManyToMany
private Set<Topic> topics = new HashSet<>();
}

  对于这种关系类型,默认数据加载方法是LAZY:每次请求文章数据时,主题列表都不会被检索。

尽量减少EAGER关系的使用

  目标是从数据库中仅加载所需数据以获取所要求的数据。例如,如果您想按应用程序中注册的名称显示作者列表,则不需要获取所有关系的数据:如他们编写的书籍,地址等。

  一个好的做法是尽量减少自动加载的关系(例如,避免使用Eager),事实上,你拥有EAGER关系越多,你将获取的对象就越不一定有用。这意味着增加了数据库所需的往返次数,并增加了专用于数据库表到应用程序实体之间映射的时间。因此,优先使用LAZY关系并只在需要时才加载对应关系的数据可能会更好。

  具体而言,建议只使用EAGER加载那些你确定始终会用的的关系数据(我认为它并不常见)。这意味着要为两个关系@OneToMany和@ManyToMany设置默认的加载方法,并为两个关系@OneToOne和@ManyToOne强制加载LAZY。这反映在指定关系的fetch属性中:

@Entity
public class Product implements Serializable {
@ManyToOne(fetch = FetchType.LAZY)
private ProductCategory category;
}

  这需要对每个实体和每个关系进行额外的调整工作,为此我们需要创建新的方法,这些方法将允许我们在最少的查询中加载所需的所有数据。事实上,如果需要显示与作者有关​​的所有数据(他的书目录,他的地址等),那么在一次查询中获取该对象及其关系将会很重要,可以使用join去解决。

如何控制会执行哪些查询

  Spring Data JPA为我们提供了访问数据的途径。但是,您必须知道这些是如何实现的。要验证执行哪些查询以从数据库检索数据,必须启动Hibernate的日志。

  其中一个方式是添加如下配置

spring:
jpa:
show-sql: true

如何优化LAZY对象的检索

  Spring Data JPA提供了指定在数据库中的选择查询期间将加载哪些关系的功能。我们将用几种方法来看看相同的示例:如何在单个查询中检索包含其主题的文章。

方法1:使用@Query检索和加载对象

  @Query注解允许使用JPQL语言编写选择查询。因此,您可以使用join中使用JPQL关键字fetch 来加载这些关系。

  在Article实体中,可以通过指定在检索到的文章的实例中加载主题列表来创建方法findOneWithTopicsById:

@Repository
public interface ArticleRepository extends JpaRepository<Article,Long> {
@Query("select article from Article article left join fetch article.topics where article.id =:id")
Article findOneWithTopicsById(@Param("id") Long id);
}

方法2:使用@EntityGraph检索和加载对象

  从Spring Data JPA的1.10版开始,您可以使用@EntityGraph注解来创建关系图,以在请求时加载实体。

  这个注解也被用在JPA repositories中。可以直接在repository的查询上或实体上定义。

在查询中定义

  我们定义了由关键字attributePaths代表关系列表的关系(这里是一个元素的列表):

@Repository
public interface ArticleRepository extends JpaRepository<Article,Long> {
@EntityGraph(attributePaths = "topics")
Article findOneWithTopicsById(Long id);
}

在实体中定义

  从JPA 2.1开始,可以使用命名实体视图在实体上定义这些图。主要优点是可以在多个查询中使用此视图。在这种情况下,我们使用关键字attributeNodes指定加载关系的列表,该关键字是@NamedAttributeNode的列表。下面看下如何在Article实体中实现它。

@Entity
@NamedEntityGraph(name = "Article.topics", attributeNodes = @NamedAttributeNode("topics"))
public class Article implements Serializable {
...
}

  可以按照如下的方式使用它:

@Repository
public interface ArticleRepository extends JpaRepository<Article,Long> {
@EntityGraph(value = "Article.topics")
Article findOneWithTopicsById(Long id);
}

  此外,可以使用属性类型指定非指定关系的加载类型:对所有非指定关系进行LAZY加载或默认加载。

  也可以创建子图,从而以分层方式工作以尽可能的减少不比较的加载。

@EntityGraph的使用限制

  对于这两种与实体图相关的方法,我们不能检索包含所有具有关系的实体的列表。事实上,我们可能想创建一个方法,例如将其定义为findAllWithTopics(),来获取所有的文章和文章对应的主题 列表。这是不可以的,必须使用搜索限制(如使用where来筛选)。

  为了克服这个限制,一个解决方案是创建一个方法findAllWithTopicsByIdNotNull():该id永远不为null,所有的数据将被检索。另一种方法是使用第一种方法@Query来执行join查询,因为@Query注解没有此限制。

如果有必要可以添加非可选信息

  当@OneToOne或@ManyToOne关系是强制性的 - 也就是说,实体必须具有关联关系 - 告知Spring Data JPA这种关系不是可选的,是很重要的。

  我们可以举一个例子:一个人必须有一个地址,它本身可以被几个人共享。所以,关系的定义如下:

@Entity
public class Person implements Serializable {
@ManyToOne(optional = false)
@NotNull
private Adress adress;
}

  添加optional = false信息将允许Spring Data JPA在创建其查询语句时更高效,因为它知道person一定会关联一个非空的地址。因此,在定义强制关系时始终指定此属性是一种好的做法。

注意事项

  尽管将关系从EAGER加载换成LAZY加载可能会提高性能,但它也会产生一些意想不到的后果,并且可能会出现一些错误。这里有两个很常见的例子。

可能的信息丢失

  第一个副作用可能是信息丢失,例如通过Web服务发送实体时。

  例如,当我们修改Person和Address之间从EAGER到LAZY之间的关系时,我们必须重新检查获取Person实体的查询语句,将address属性一起查询出来(使用上述方法之一)。否则,Person Web服务可能仅提供特定于该Person的数据,并且address数据可能已丢失。

  这是一个问题,因为Web服务可能不再满足其接口协议。例如,它可能会影响网页上的显示:需要将地址显示在HTML中。

  为了避免这个问题,使用数据传输对象(DTO)而不是直接将实体返回给客户端是很有必要的。实际上,映射器将实体转换为DTO将通过在数据库中检索初始查询期间尚未加载的关系来加载它需要的所有关系:这就是Lazy Loading。因此,即使我们不重构实体,Web服务也将继续返回相同的数据。

可能的事务问题

  第二个副作用可以是LazyInitializationException。

  尝试在事务外加载关系时发生此异常。无法完成懒加载,因为对象已分离:它不再属于Hibernate会话。当把EAGER改为LAZY时,可能会发生这个异常。

  在这种情况下,可能出现的例外情况有两个主要原因:

  1. 第一个原因可能是你不在Hibernate事务中。在这种情况下,您必须使流程事务化(在该方法或其类上设置@Transactional注接)或调用可以负责加载依赖关系的事务服务。
  2. 第二个原因可能是在事务之外处理了懒加载对象,且该实体未附加到您的新事务中。在这种情况下,您必须在第一个事务中加载关系,或者在第二个事务中重新连接对象。

分页查询的特性

  当你想创建一个包含来自一个或多个关系信息的分页查询时,直接从一个查询语句中就加载其关联的属性的数据是一个(非常)不好的主意。例如,当我们检索包含主题的文章的第一页时,最好不要直接加载所有文章+主题数据,而是首先加载文章中的数据,然后加载与主题相关的数据。

  事实上,如果不这样做,应用程序将被迫建立2个表之间的的完整数据集,将它们存储在内存中,然后只选择所请求页面的数据。这与数据库的工作方式直接相关:即使我们只需要数据的一部分(一页数据,最小/最大的结果,结果的前几个),它们也必须join的所有数据。

  在加载页面及其关系的情况下,日志中会显示一条明确的消息来警告您:

HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!

  2个表的体积越大,影响越大:对于包含数百万个条目的表,这可能导致应用程序的处理非常昂贵。

  为了克服这个问题,首先加载不包含关系的实体,然后在第二步中再次加载相关联的实体。要加载请求页面的实体数据,可以使用JPA存储库的findAll(Pageable pageable)方法(从PagingAndSortingRepository类继承),然后,要加载关系数据,可以使用Lazy Loading直接调用关系的getter来检索每个实体的数据(需要在事务中)。

  但是这个操作会很昂贵,因为它会产生很多查询。事实上,对于每个实体,将会有许多选择查询,因为有加载关系:这个问题被称为“Hibernate N + 1查询问题”。如果我们以加载包含关系的20篇文章的页面为例进行加载,则这将导致21个查询:页面为1,每篇文章的主题为20。

  为了降低成本,可以在两个实体之间的@OneToMany或@ManyToMany关系上使用@BatchSize(size = n)注释。这允许Hibernate在数据库中进行选择查询之前等待足够的(n个)关系进行检索。这个数字n要与页面的大小相关联(但它意味着有一个默认的页面大小,因为n是在实体上定义的,因此是恒定的)。在前面的例子中,我们可以将最小数字指定为20:

@Entity
public class Article implements Serializable {
@ManyToMany
@BatchSize(size = 20)
private Set<Topic> topics = new HashSet<>();
}

  在这种情况下,加载页面的查询数将从21减少到2,一次加载所有文章,另一次加载所有主题。

  注意:如果页面包含少于20个元素(小于n),则主题仍将被正确加载。

原文链接  https://blog.ippon.tech/boost-the-performance-of-your-spring-data-jpa-application/

提高spring boot jpa性能(译)的更多相关文章

  1. Spring Boot(五):Spring Boot Jpa 的使用

    在上篇文章Spring Boot(二):Web 综合开发中简单介绍了一下 Spring Boot Jpa 的基础性使用,这篇文章将更加全面的介绍 Spring Boot Jpa 常见用法以及注意事项. ...

  2. Spring Boot Jpa 的使用

    Spring Boot Jpa 介绍 首先了解 Jpa 是什么? Jpa (Java Persistence API) 是 Sun 官方提出的 Java 持久化规范.它为 Java 开发人员提供了一种 ...

  3. (转)Spring Boot(五):Spring Boot Jpa 的使用

    http://www.ityouknow.com/springboot/2016/08/20/spring-boot-jpa.html 在上篇文章Spring Boot(二):Web 综合开发中简单介 ...

  4. spring boot jpa 使用update 报错解决办法

    在spring boot jpa 中自定义sql,执行update操作报错解决办法: 在@Query(...)上添加 @Modifying@Transactional注解

  5. Spring Boot + JPA(hibernate 5) 开发时,数据库表名大小写问题

      (转载)Spring Boot + JPA(hibernate 5) 开发时,数据库表名大小写问题   这几天在用spring boot开发项目, 在开发的过程中遇到一个问题hibernate在执 ...

  6. Spring boot JPA 用自定义主键策略 生成自定义主键ID

    最近学习Spring boot JPA 学习过程解决的一些问题写成随笔,大家一起成长.这次遇到自定义主键的问题 package javax.persistence; public enum Gener ...

  7. Spring Boot(十五):spring boot+jpa+thymeleaf增删改查示例

    Spring Boot(十五):spring boot+jpa+thymeleaf增删改查示例 一.快速上手 1,配置文件 (1)pom包配置 pom包里面添加jpa和thymeleaf的相关包引用 ...

  8. spring boot JPA中实体类常用注解

    spring boot jpa中的注解很多,参数也比较多.没必要全部记住,但是经常查看官方文档也比较麻烦,记录一下一些常用的注解.通过一些具体的例子来帮助记忆. @Entity @Table(name ...

  9. spring boot + jpa + kotlin入门实例

    spring boot +jpa的文章网络上已经有不少,这里主要补充一下用kotlin来做. kotlin里面的data class来创建entity可以帮助我们减少不少的代码,比如现在这个User的 ...

随机推荐

  1. Qt 多线程同步与通信

    Qt 多线程同步与通信 1 多线程同步 Qt提供了以下几个类来完成这一点:QMutex.QMutexLocker.QSemphore.QWaitCondition. 当然可能还包含QReadWrite ...

  2. 【BZOJ2339】卡农(递推,容斥)

    [BZOJ2339]卡农(递推,容斥) 题面 BZOJ 题解 先简化一下题意: 在\([1,2^n-1]\)中选择不重复的\(m\)个数,使得他们异或和为\(0\)的方案数. 我们设\(f[i]\)表 ...

  3. 洛谷P1242 新汉诺塔 【神奇的递归】

    题目描述 设有n个大小不等的中空圆盘,按从小到大的顺序从1到n编号.将这n个圆盘任意的迭套在三根立柱上,立柱的编号分别为A.B.C,这个状态称为初始状态. 现在要求找到一种步数最少的移动方案,使得从初 ...

  4. 洛谷 P1325 雷达安装 解题报告

    P1325 雷达安装 题目描述 描述: 假设海岸线是一条无限延伸的直线.它的一侧是陆地,另一侧是海洋.每一座小岛是在海面上的一个点.雷达必须安装在陆地上(包括海岸线),并且每个雷达都有相同的扫描范围d ...

  5. 解题:NOIP 2018 赛道修建

    题面 几乎把我送退役的一道题,留在这里做个纪念. 考场看出来是原题结果为了求稳强行花了一个小时写了80pts暴力,然后挂了55pts(真·暴力写挂),结果今天花了不到半个小时连想带写一遍95pts(T ...

  6. SAS数据步与过程步,数据步语句

    SAS数据步与过程步,数据步语句http://www.biostatistic.net/thread-2045-1-1.html  ---转载---原文作者:biostar(出处: 生物统计家园) 数 ...

  7. Java Web 生成临时文件并下载

    转自: Java Web 生成临时文件并下载 概述:本文是  java 服务器端生成文件并下载的示例,并不完善,下载之后一般来说还需要删除临时文件. 注意:临时文件存放在 /WEB-INF/tmp 目 ...

  8. 「Django」rest_framework学习系列-分页

    分页a.分页,看第N页,每页显示N条数据方式一:使用PageNumberPagination创建分页对象,配合settings全局配置 views设置 from rest_framework.pagi ...

  9. [Java多线程]-线程池的基本使用和部分源码解析(创建,执行原理)

    前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 多线 ...

  10. Java常量池详解之Integer缓存

    一个Java question,求输出结果   public class IntegerTest { public static void main(String[] args) { objPoolT ...