编程

Hibernate 最佳实践

313 2024-07-22 14:00:00

Hibernate 是迄今为止最受欢迎的 JPA 实现。这种受欢迎程度为所有用户带来了几个优势。有很多关于它的博客文章,流行论坛上的问答,以及公认的最佳实践。本文将总结一些 JPA 和 Hibernate 的最佳实践,

1. 使用适合用例的投影

编写 SQL SELECT 语句时,显然你只是在为用例检索所需的字段。当使用 Hibernate 时,这应该没有什么不同。不幸的是,许多开发人员只从数据库中选择实体,无论它是否适合用例。

JPA 和 Hibernate 支持更多的投影,而不仅仅是实体。它们有三种不同的类型,每种都有其优点和缺点:

1.1 实体

实体是最常见的投影。当你需要实体的所有属性以及只影响少数实体的更新或删除操作时,应该使用它。

em.find(Author.class, 1L);

1.2 POJO

POJO 投影类似于实体投影,但它允许你创建数据库记录的特定于用例的表示。如果你只需要实体属性的一小部分,或者需要几个关联实体的属性,这尤其有用。

List<BookPublisherValue> bookPublisherValues = em.createQuery(

  “SELECT new org.thoughts.on.java.model.BookPublisherValue(b.title, b.publisher.name) FROM Book b”,

BookPublisherValue.class).getResultList();

1.3 标量值

标量值不是一种非常流行的投影类型,因为它将值表示为 Object[]。只有当你想选择少量属性并在业务逻辑中直接处理它们时,才应该使用它。当必须检索更多的属性,或者如果想将查询结果传输到不同的子系统时,POJO 投影通常是更好的选择。

List<Object[]> authorNames = em.createQuery(

“SELECT a.firstName, a.lastName FROM Author a”).getResultList();

2. 使用适合用例的查询类型

JPA 和 Hibernate 提供了多种隐式和显式选项来定义查询。它们并不适合每个用例,因此,请确保选择最适合的用例。

2.1 EntityManager.find()

EntityManager.find()  方法不仅是通过主键获取实体的最简单方法,而且它还提供了性能和安全优势:

  • Hibernate 在执行 SQL 查询以从数据库中读取实体之前,会检查一级和二级缓存。
  • Hibernate 生成查询并将主键值设置为参数,以避免 SQL 注入漏洞。
em.find(Author.class, 1L);

2.2 JPQL

Java 持久化查询语言(JPQL)是由 JPA 标准定义的,与 SQL 非常相似。它对实体及其关系而不是数据库表进行操作。你可以使用它来创建低复杂度和中等复杂度的查询。

TypedQuery<Author> q = em.createQuery(

  “SELECT a FROM Author a JOIN a.books b WHERE b.title = :title”,

Author.class);

2.3 Criteria API

Criteria API 是一个很容易在运行时动态定义查询的 API。如果查询的结构取决于用户输入,则应该使用这种方法。你可以在下面的代码中看到这样一个查询的示例。如果输入对象的 title 属性包含非空 String,则 Book 实体将与 Author 实体连接,并且 title 必须等于输入参数。

CriteriaBuilder cb = em.getCriteriaBuilder();

CriteriaQuery<Author> q = cb.createQuery(Author.class);

Root<Author> author = q.from(Author.class);

q.select(author);

if (!input.getTitle().isEmpty()) {

  SetJoin<Author, Book> book = author.join(Author_.books);

  q.where(cb.equal(book.get(Book_.title), input.getTitle()));

}

2.4 原生查询

原生查询为你提供了编写和执行纯 SQL 语句的机会。对于高度复杂的查询,如果你想使用特定于数据库的功能,如 PostgreSQL 的 JSONB 数据类型,这通常是最好的方法。

MyEntity e = (MyEntity) em.createNativeQuery(

  “SELECT * FROM myentity e WHERE e.jsonproperty->’longProp’ = ‘456’“,

MyEntity.class).getSingleResult();

关于原生查询,可查阅此文

3. 使用参数绑定

你应该为查询参数使用参数绑定,而不是直接将值添加到查询字符串中。这提供了几个优点:

  • 你无需担心 SQL 注入,
  • Hibernate 将查询参数映射到正确的类型上,而且
  • Hibernate 可以内部优化以提供更好的性能。

JPQL、Criteria API 和 原生 SQL 查询使用同一个 Query 接口,该接口提供了一个 setParameter 方法用来定位和命名参数绑定。Hibernate 支持原生查询的命名参数绑定,而 JPA 规范未对此进行定义。因此,我建议你在原生查询中仅使用位置参数。它们使用“?”引用,编号从1开始。

Query q = em.createNativeQuery(“SELECT a.firstname, a.lastname FROM Author a WHERE a.id = ?”);

q.setParameter(1, 1);

Object[] author = (Object[]) q.getSingleResult();

Hibernate 和 JPA 支持 JPQL 和 Criteria API 的命名参数绑定。这允许你为每个参数定义一个名称,并将其提供给 setParameter 方法以将值绑定到该方法。该名称区分大小写,需要以“:”符号为前缀。

Query q = em.createNativeQuery(“SELECT a.firstname, a.lastname FROM Author a WHERE a.id = :id”);

q.setParameter(“id”, 1);

Object[] author = (Object[]) q.getSingleResult();

4. 对命名查询和参数名称使用静态字符串

这只是一件小事,但如果将命名查询及其参数的名称定义为静态字符串,那么使用它们会容易得多。我更喜欢将它们定义为可以使用它们的实体的属性,但也可以创建一个包含所有查询和参数名称的类。

@NamedQuery(name = Author.QUERY_FIND_BY_LAST_NAME,

query = “SELECT a FROM Author a WHERE a.lastName = :” + Author.PARAM_LAST_NAME)

@Entity

public class Author {

  public static final String QUERY_FIND_BY_LAST_NAME = “Author.findByLastName”;

  public static final String PARAM_LAST_NAME = “lastName”;

  …

}

你可以使用这些字符串去实例化命名查询并设置参数。

Query q = em.createNamedQuery(Author.QUERY_FIND_BY_LAST_NAME);

q.setParameter(Author.PARAM_LAST_NAME, “Tolkien”);

List<Author> authors = q.getResultList();

5. 使用 Criteria API 时使用 JPA 元模型

Criteria API 提供了一种在运行时动态定义查询的舒适方法。这需要引用实体及其属性。最好的方法是使用静态 JPA 元模型。你可以在构建时为每个实体自动生成一个静态元模型类。此类为每个实体属性都包含一个静态属性。

@Generated(value = “org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor”)

@StaticMetamodel(Author.class)

public abstract class Author_ {

  public static volatile SingularAttribute<Author, String> firstName;

  public static volatile SingularAttribute<Author, String> lastName;

  public static volatile SetAttribute<Author, Book> books;

  public static volatile SingularAttribute<Author, Long> id;

  public static volatile SingularAttribute<Author, Integer> version;

}

然后你可以使用元模型(metalmodel)类去引用在 Criteria 查询中的实体。我在下面代码的第 5 行中使用它来引用 Author 实体的 lastName 属性。

CriteriaBuilder cb = em.getCriteriaBuilder();

CriteriaQuery<Author> q = cb.createQuery(Author.class);

Root<Author> author = q.from(Author.class);

q.select(author);

q.where(cb.equal(author.get(Author_.lastName), lastName));

6. 使用代理键并让 Hibernate 生成新值

代理主键(或技术 ID)的主要优点是它是一个简单的数字,而不是大多数自然键的多个属性的组合。所有涉及的系统,主要是 Hibernate 和数据库,都可以非常有效地处理它。Hibernate 还可以使用现有的数据库功能,如序列(sequence)或自动递增字段,为新实体生成唯一值。

@Id

@GeneratedValue

@Column(name = “id”, updatable = false, nullable = false)

private Long id;

7. 指定自然标识符

即使决定使用代理键作为主键,你也应该指定自然标识符。然而,一个自然标识符标识了数据库记录和现实世界中的对象。许多用例使用它们来代替人工的代理键。因此,将它们建模为数据库中的唯一键是一种很好的做法。Hibernate 还允许将它们建模为实体的自然标识符,并提供额外的 API 用于从数据库中检索它们。

为属性建模所要做的唯一一件事就是创建一个自然 id,也就是用 @NaturalId 对其进行注释。

@Entity

public class Book {

  @Id

  @GeneratedValue(strategy = GenerationType.AUTO)

  @Column(name = “id”, updatable = false, nullable = false)

  private Long id;

  @NaturalId

  private String isbn;

  …

}

8. 使用 SQL 脚本来创建数据库 schema

Hibernate 可以使用实体的映射信息来生成数据库 Schema。这是最简单的方法,你可以在互联网上的一些例子中看到。这对于小型测试应用来说可能没问题,但不应该将其用于业务应用。数据库 schema 对数据库的性能和大小有着巨大的影响。因此,你应该自己设计和优化数据库 schema,并将其导出为 SQL 脚本。你可以使用像 Flyway 这样的外部工具运行此脚本,也可以在启动时使用 Hibernate 初始化数据库。下面的代码显示了一个 persistence.xml 文件,该文件告诉 Hibernate 运行 create.sql 脚本来设置数据库。你可以在 JPA 2.1 的标准化 schema 生成和数据加载中了解更多关于不同配置参数的信息。

<?xml version=”1.0″ encoding=”UTF-8″ standalone=”yes”?>

<persistence xmlns=”http://xmlns.jcp.org/xml/ns/persistence” xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” version=”2.1″ xsi:schemaLocation=”http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd”>

  <persistence-unit name=”my-persistence-unit” transaction-type=”JTA”>

    <description>My Persistence Unit</description>

    <provider>org.hibernate.ejb.HibernatePersistence</provider>

    <jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>

    <exclude-unlisted-classes>false</exclude-unlisted-classes>

    <properties>

      <property name=”hibernate.dialect” value=”org.hibernate.dialect.PostgreSQLDialect”/>

      <property name=”javax.persistence.schema-generation.scripts.action” value=”create”/>

      <property name=”javax.persistence.schema-generation.scripts.create-target” value=”./create.sql”/>

    </properties>

  </persistence-unit>

</persistence>

9. 记录和分析开发过程中的所有查询

执行过多的查询是造成 Hibernate 性能问题的最常见原因。这通常是由 n+1 检索问题引起的,但这并不是触发比预期更多 SQL 语句的唯一方法。

Hibernate 将所有数据库交互隐藏在其 API 后面,并且通常很难猜测它将为给定的用例执行多少查询。处理此问题的最佳方法是在开发过程中记录所有 SQL 语句,并在完成实现任务之前对其进行分析。你可以通过将 org.hibernate.SQL 日志级别设置为 DEBUG 来实现这一点。

10. 别使用 FetchType.EAGER

立刻获取(Eager fetch)是 Hibernate 性能问题的另一个常见原因。当它从数据库中获取实体时,它告诉 Hibernate 初始化关联。

@ManyToMany(mappedBy = “authors”, fetch = FetchType.EAGER)

private Set<Book> books = new HashSet<Book>();

Hibernate 如何从数据库中获取关联实体取决于关联和定义的 FetchMode。但这不是主要问题。主要问题是,Hibernate 将获取关联实体,无论它们是否是给定用例所必需的。这会产生开销,从而降低应用的速度,并经常导致性能问题。你应该使用 FetchType.LAZY,并且仅当用例需要关联实体时才获取它们。

@ManyToMany(mappedBy = “authors”, fetch = FetchType.LAZY)

private Set<Book> books = new HashSet<Book>();

11.使用初始查询初始化所需的惰性关联

正如我前面所解释的,FetchType.LAZY 告诉 Hibernate 只有在关联实体被使用时才获取它们。这有助于你避免某些性能问题。但这也是 LazyInitializationException 和 n+1 检索问题的原因,当 Hibernate 必须执行额外的查询来初始化所选 n 个实体中的每个实体的关联时,就会出现这种问题。

避免这两个问题的最佳方法是将一个实体与用例所需的关联放在一起。这样做的一个方式是使用带有 JOIN FETCH 语句的 JPQL 查询。

List<Author> authors = em.createQuery(

  “SELECT DISTINCT a FROM Author a JOIN FETCH a.books b”,

Author.class).getResultList();

12. 避免大型关联的级联删除

大多数开发人员(包括我自己)在看到关联的 CascadeType.REMOVE 定义时都会有点紧张。它告诉 Hibernate 在删除这个实体时也要删除关联的实体。人们总是担心关联实体也会对其某些关联使用级联删除,并且 Hibernate 可能会删除比预期更多的数据库记录。在我使用 Hibernate 这些年里,这种情况从未发生在我身上,我不认为这是一个真正的问题。但是级联删除会让人很难理解如果删除实体会发生什么。这是你应该永远避免的事情。如果你仔细研究一下 Hibernate 是如何删除关联实体的,你会发现另一个避免它的原因。Hibernate 为每个相关实体执行 2 个SQL语句:1 个 SELECT 语句从数据库中提取实体,1 个 DELETE 语句删除实体。如果只有 1 或 2 个关联实体可能没问题,但如果有大量相关实体,则会产生性能问题。

13. 尽可能使用 @Immutable

Hibernate 定期对与当前 PersistenceContext 关联的所有实体执行脏检查,以检测所需的数据库更新。这对所有可变实体来说都是一件好事。但并不是所有的实体都必须是可变的。实体也可以映射只读数据库视图或表。对这些实体执行任何脏检查都是应该避免的开销。可以通过使用 @Immutable 注释实体来实现此操作。然后,Hibernate 将在所有脏检查中忽略它,并且不会向数据库写入任何更改。

@Entity

@Immutable

public class BookView {

  …

}

总结

我介绍了一系列最佳实践,这些实践可以帮你更快地实现应用,并避免常见的性能陷阱。我自己也遵循它们来避免这些问题,它们对我帮助很大。

在使用 JPA 和 Hibernate 时,你遵循了哪些最佳实践呢?请在下面发表评论并告诉我。