Hibernate ORM 中文操作指南

5. Interacting with the database

若要与数据库交互,也就是说要执行查询或插入、更新或删除数据,我们需要以下某个对象的实例:

  1. a JPA EntityManager,

  2. a Hibernate Session, or

  3. a Hibernate StatelessSession.

Session 接口扩展了 EntityManager,所以两个接口之间的唯一区别在于,Session 提供了一些其他操作。

实际上,在 Hibernate 中,每个 EntityManager 都是 Session,你可以这样缩小范围:

Session session = entityManager.unwrap(Session.class); Session session = entityManager.unwrap(Session.class);

Session(或 EntityManager)的实例是 stateful session。它通过 persistence context 上的操作在你的程序和数据库之间进行调解。

在本章中,我们不会过多地讨论 StatelessSession。当我们讨论性能时,我们会回到 this very useful API。你现在需要了解的是无状态会话没有持久性上下文。

不过,我们应告知您,有些人喜欢在任何地方都使用 StatelessSession 。它是一种更简单的编程模型,可以让开发者与数据库以更多 directly 的方式进行交互。

有状态会话当然有其优势,但它们更难以分析,当出现错误时,错误信息可能更难以理解。

5.1. Persistence Contexts

持久性上下文是一种缓存;有时我们称之为“一级缓存”,以将其与 second-level cache 区分开来。对于在持久性上下文的范围内从数据库中读取的每个实体实例,以及在持久性上下文的范围内持久化的每个新实体,该上下文都持有从实体实例的标识符到该实例本身的唯一映射。

因此,一个实体实例可以通过三种状态之一与给定的持久性上下文相关联:

  • transient — never persistent, and not associated with the persistence context,

  • persistent——当前与持久性上下文相关联,或

  • detached——以前在另一个会话中持久化,但当前未与 this 持久性上下文相关联。

entity lifecyle

在任意给定时刻,一个实例可以与最多一个持久性上下文关联。

持久性上下文的使用寿命通常与事务的使用寿命相对应,尽管可以拥有跨越几个数据库级别事务(这些事务形成一个逻辑工作单元)的持久性上下文。

持久化上下文——即 SessionEntityManager ——绝不能在多个线程或并行事务之间共享。

如果你不小心泄露了线程间的会话,你就会遭殃。

我们喜欢持久性上下文的理由有很多。

  • 它们有助于避免 data aliasing:如果我们在代码的一个部分修改某个实体,那么在同一个持久性上下文中执行的其他代码会看到我们的修改。

  • 它们允许 automatic dirty checking:修改实体后,我们无需执行任何显式操作要求 Hibernate 将该更改传播回数据库。相反,当会话被 flushed 时,该更改将自动与数据库同步。

  • 通过避免在给定工作单元中反复请求给定的实体实例,它们可以提高性能。

  • 它们使同时 transparently batch 多个数据库操作成为可能。

持久性上下文还允许我们检测在执行实体图操作时是否存在循环。(即使在无状态会话中,我们需要某种实体实例的临时缓存,以便在执行查询时使用。)

另一方面,有状态会话有一些非常重要的限制,因为:

  1. 持久性上下文不是线程安全的,不能跨线程共享,

  2. 并且一个持久性上下文不能在不相关的交易之间复用,因为这会破坏交易的隔离性和原子性。

此外,持久性上下文拥有所有实体的硬链接,从而防止这些实体被垃圾回收。因此,一旦工作单元完成,会话必须被丢弃。

如果您并未完全理解前面的文字,请返回并反复阅读,直到理解为止。用户错误地管理 Hibernate Session 或 JPA EntityManager 的声明周期,导致大量人类遭受痛苦。

最后我们需要注意的是,持久性上下文是否有助于或损害给定工作单元的性能在很大程度上取决于工作单元的性质。出于这个原因,Hibernate 提供了有状态和无状态会话。

5.2. Creating a session

坚持标准 JPA 定义的 API,我们看到了如何在 Configuration using JPA XML 中获取 EntityManagerFactory。我们可能会使用此对象创建一个 EntityManager:这不足为奇:

EntityManager entityManager = entityManagerFactory.createEntityManager();

在完成 EntityManager 后,我们应该显式地清理它:

entityManager.close();

另一方面,如果我们从 SessionFactory 开始,如 Configuration using Hibernate API 中所述,我们可能会使用:

Session session = sessionFactory.openSession();

但我们仍需要清理:

session.close();

在容器环境外,我们还必须编写代码来管理数据库事务。

5.3. Managing transactions

使用 JPA 标准 API,通过 EntityTransaction 接口我们可以控制数据库事务。我们推荐的习惯用法如下:

EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction tx = entityManager.getTransaction();
try {
    tx.begin();
    //do some work
    ...
    tx.commit();
}
catch (Exception e) {
    if (tx.isActive()) tx.rollback();
    throw e;
}
finally {
    entityManager.close();
}

使用 Hibernate 的本机 API,我们可以编写一些非常相似的代码,但由于此类代码非常繁琐,我们有一个更好的选择:

sessionFactory.inTransaction(session -> {
    //do the work
    ...
});

JPA 没有标准的方法来设置事务超时,但 Hibernate 有:

session.getTransaction().setTimeout(30); // 30 seconds

5.4. Operations on the persistence context

当然,我们需要 EntityManager 的主要原因是对数据库执行操作。下列重要操作使我们能够与持久性上下文交互,并计划对数据的修改:

表 34. 修改数据和管理持久性上下文的方法

Method name and parameters

Effect

persist(Object)

使瞬态对象持久化,并计划一个 SQL insert 语句供稍后执行

remove(Object)

使持久化对象瞬态,并计划一个 SQL delete 语句供稍后执行

merge(Object)

将给定分离对象的 state 复制到一个对应的受管持久化实例,并返回持久化对象

detach(Object)

在不影响数据库的情况下使持久化对象与 session 解关联

clear()

清空持久化上下文并分离其所有 entities

flush()

检测对 session 关联的持久化对象所做的更改,并通过执行 SQL insertupdatedelete 语句使数据库 state 与 session 的 state 同步

请注意,persist()remove() 对数据库没有直接的影响,而是仅为稍后执行计划一条命令。另请注意,无状态会话没有任何 update() 操作。在 flushed 会话时会自动检测到修改。

另一方面,除了 getReference(),下列操作都会导致立即访问数据库:

表 35. 读取和锁定数据的方法

Method name and parameters

Effect

find(Class,Object)

根据给定类型和 id 获取持久化对象

find(Class,Object,LockModeType)

在发出给定 optimistic or pessimistic lock mode请求后,根据其类型和 ID 获取永续对象

getReference(Class,id)

根据给定类型和 id 获取对持久化对象的引用,而无需实际从数据库加载其 state

getReference(Object)

根据给定分离实例获取具有相同标识的持久化对象的引用,而无需实际从数据库加载其 state

refresh(Object)

刷新对象持久态,使用新的 SQL select 从数据库中检索其最新状态

refresh(Object,LockModeType)

使用新的 SQL _select_来刷新对象的永续状态以从数据库中检索其当前状态,发出给定 optimistic or pessimistic lock mode请求

lock(Object, LockModeType)

在永续对象上获取 optimistic or pessimistic lock

任何这些操作都可能抛出异常。现在,如果在与数据库进行交互时发生异常,则没有好的方法重新同步当前持久性上下文的状态和数据库表中保存的状态。

因此,认为在任何方法抛出异常后,会话都无法使用。

持久化上下文很脆弱。如果您收到 Hibernate 的异常,您应当立即关闭并放弃当前会话。如果您需要的话,请打开一个新会话,但首先抛弃有问题的会话。

到目前为止我们看到的每一个操作都会影响一个作为参数传递的单个实体实例。但是有一种方法可以设置事物,以便操作将传播到关联实体。

5.5. Cascading persistence operations

child 实体的生命周期通常完全依赖于_parent_ 的生命周期。这对于多对一和一对一关联尤其常见,尽管对于多对多关联来说非常罕见。

例如,建立一个 Order 及其全部 Item_s persistent in the same transaction, or to delete a _Project 及其 Files_s at once. This sort of relationship is sometimes called a _whole/part 类型关系的情况非常普遍。

Cascading 是一种便利,允许我们将 Operations on the persistence context 中列出的其中一项操作从父级传播到其子级。要设置级联,我们指定关联映射注释之一(通常是 @OneToMany@OneToOne)的 cascade 成员。

@Entity
class Order {
    ...
    @OneToMany(mappedby=Item_.ORDER,
               // cascade persist(), remove(), and refresh() from Order to Item
               cascade={PERSIST,REMOVE,REFRESH},
               // also remove() orphaned Items
               orphanRemoval=true)
    private Set<Item> items;
    ...
}

Orphan removal 表明如果一个 Item 从属于其父代 Order 的项集合中删除,则该 Item 应该自动删除。

5.6. Proxies and lazy fetching

我们的数据模型是一组相互关联的实体,在 Java 中,我们的整个数据集将表示为一个相互关联的巨大对象图。此图有可能断开,但更有可能它是连接的,或由数量较小的连接子图组成。

因此,当我们就此图的一个对象从数据库中检索并把它实例化到内存中,我们根本无法递归检索并实例化它所有关联的实体。除了浪费虚拟机端的内存外,此过程还将涉及大量的到数据库服务器的往返行程,或巨大的多维笛卡尔乘积表,或两者兼有。相反,我们被迫在某处切断图形。

Hibernate 使用 proxieslazy fetching 解决此问题。代理是一个对象,它伪装成一个真正的实体或集合,但不实际保存任何状态,因为尚未从数据库中提取该状态。当你调用代理方法时,Hibernate 将检测调用的状态并从数据库中提取该状态,然后才允许调用继续到真实的实体对象或集合。

现在说说需要注意的事项:

  • Hibernate 仅会对当前与持久性上下文关联的实体执行此操作。一旦会话结束,持久性上下文被清除,代理将不再可获取,而它的方法会抛出令人痛恨的 LazyInitializationException

  • 对于多态关联,Hibernate 在实例化代理时不知道所引用实体的具体类型,因此 instanceof 和类型转换等操作应用于代理时无法正确工作。

  • 往返一次数据库来获取单个实体实例的状态只是 the least efficient 访问数据的方式。它几乎不可避免地会导致臭名昭著的 N+1 selects 问题,我们稍后讨论如何 optimize association fetching 时会谈论这个问题。

@ConcreteProxy 注解解决了陷阱 2,但是以性能为代价(额外联接),因此通常不建议使用,除非在非常特殊的情况下。

我们在这里有点超前了,但让我们快速提一下我们建议驾驭这些陷阱的总体策略:

所有关联都应被设置为 fetch=LAZY,以避免在不需要时获取额外数据。正如我们在 earlier 中提到的,此设置不是 @ManyToOne 关联的默认设置,必须明确指定。

但是,努力避免编写触发延迟获取的代码。相反,在工作单元开始时预先获取您需要的所有数据,使用 Association fetching 中描述的技术之一,通常在 HQL 中使用 join fetchEntityGraph

重要的是要知道,某些操作可能使用未提取代理 don’t 执行,需要从数据库中提取其状态。首先,我们始终被允许获取其标识符:

var pubId = entityManager.find(Book.class, bookId).getPublisher().getId(); // does not fetch publisher

其次,我们可以创建指向代理的关联:

book.setPublisher(entityManager.getReference(Publisher.class, pubId)); // does not fetch publisher

有时,测试是否从数据库中提取了代理或集合很有用。JPA 让我们使用 PersistenceUnitUtil 执行此操作:

boolean authorsFetched = entityManagerFactory.getPersistenceUnitUtil().isLoaded(book.getAuthors());

Hibernate 有一个更简单的执行此操作的方法:

boolean authorsFetched = Hibernate.isInitialized(book.getAuthors());

但是 Hibernate 类的静态方法让我们可以执行更多操作,花点时间熟悉它们是很有必要的。

特别感兴趣的是,这些操作让我们可以在不从数据库中提取其状态的情况下使用未提取集合。例如,考虑此代码:

Book book = session.find(Book.class, bookId);  // fetch just the Book, leaving authors unfetched
Author authorRef = session.getReference(Author.class, authorId);  // obtain an unfetched proxy
boolean isByAuthor = Hibernate.contains(book.getAuthors(), authorRef); // no fetching

此代码片段将集合 book.authors 和代理 authorRef 都留作未提取状态。

最后,Hibernate.initialize() 是强制提取代理或集合的便捷方法:

Book book = session.find(Book.class, bookId);  // fetch just the Book, leaving authors unfetched
Hibernate.initialize(book.getAuthors());  // fetch the Authors

但是,当然,此代码效率很低,需要到数据库进行两次往返才能获取原则上可以使用一次查询检索的数据。

从上面的讨论中清楚地看出,我们需要一种方法来请求关联使用数据库 join 进行 eagerly 提取,从而保护我们自己免于臭名昭著的 N+1 选择。其中一种方法是向 find() 传递 EntityGraph

5.7. Entity graphs and eager fetching

当映射一个关联 fetch=LAZY 时,默认情况下,当我们调用 find() 方法时不会提取它。我们可以通过向 find() 传递 EntityGraph 来请求立即(eagerly)提取关联。

这方面的 JPA 标准 API 有一些笨拙:

var graph = entityManager.createEntityGraph(Book.class);
graph.addSubgraph(Book_.publisher);
Book book = entityManager.find(Book.class, bookId, Map.of(SpecHints.HINT_SPEC_FETCH_GRAPH, graph));

这样做既不类型安全而且不必要地冗长。Hibernate 有更好的方法:

var graph = session.createEntityGraph(Book.class);
graph.addSubgraph(Book_.publisher);
Book book = session.byId(Book.class).withFetchGraph(graph).load(bookId);

此代码在 SQL 查询中添加了一个 left outer join,同时抓取了与 Book 关联的 Publisher

我们甚至可以在 EntityGraph 中附加其他节点:

var graph = session.createEntityGraph(Book.class);
graph.addSubgraph(Book_.publisher);
graph.addPluralSubgraph(Book_.authors).addSubgraph(Author_.person);
Book book = session.byId(Book.class).withFetchGraph(graph).load(bookId);

最后形成一个带有 four 左外部联接 的 SQL 查询。

在以上的代码示例中,类 BookAuthor 由我们之前看到过的 JPA Metamodel Generator 生成。它们让我们能够以完全类型安全的方式引用模型属性。我们将在下面讨论 Criteria queries 时再次使用它们。

JPA 指定任何给定的 EntityGraph 可以在两种不同的方式中解释。

  1. fetch graph 明确指定了应该急切加载的关联。不属于实体图的任何关联都是代理和延迟加载的,仅在需要时才加载。

  2. load graph 指定除了 fetch=EAGER 映射的关联外,还应获取实体图中的关联。

你说得没错,这些名称毫无意义。但不要担心,如果你采纳我们的建议并映射关联 fetch=LAZY,那么“抓取”图和“加载”图之间就没有区别了,所以名称无所谓。

JPA 甚至指定了一种使用注解定义命名实体图的方式。但基于注解的 API 非常冗长,根本不适合使用。

5.8. Flushing the session

每隔一段时间,就会触发一个 flush 操作,并且会话会将保存在内存中的脏状态(即与持久性上下文相关的实体状态的修改)与保存在数据库中的持久性状态同步。当然,它通过执行 SQL INSERTUPDATEDELETE 语句来执行此操作。

默认情况下,会触发刷新:

  1. 当当前交易提交时,例如,调用 Transaction.commit() 时,

  2. 在必须获取延迟状态同步内存中所持有的脏数据才能影响结果的查询执行之前,或者

  3. 当程序直接调用 flush() 时。

在以下代码中,刷新在事务提交时发生:

session.getTransaction().begin();
session.persist(author);
var books =
        // new Author does not affect results of query for Books
        session.createSelectionQuery("from Book")
                // no need to flush
                .getResultList();
// flush occurs here, just before transaction commits
session.getTransaction().commit();

但在以下代码中,刷新在查询执行时发生:

session.getTransaction().begin();
session.persist(book);
var books =
        // new Book would affect results of query for Books
        session.createSelectionQuery("from Book")
                // flush occurs here, just before query is executed
                .getResultList();
// changes were already flushed to database, nothing to flush
session.getTransaction().commit();

始终可以明确调用_flush()_:

session.getTransaction().begin();
session.persist(author);
session.flush(); // explicit flush
var books =
        session.createSelectionQuery("from Book")
                // nothing to flush
                .getResultList();
// nothing to flush
session.getTransaction().commit();

请注意,SQL 语句通常不会由 Session 接口的方法(如 persist()remove())同步执行。如果需要同步执行 SQL,StatelessSession 允许这样做。

可以通过显式设置刷新模式来控制此行为。例如,要禁用在查询执行前发生的刷新,请调用:

entityManager.setFlushMode(FlushModeType.COMMIT);
  1. Hibernate 相比 JPA,可以更严格控制 flush mode

session.setHibernateFlushMode(FlushMode.MANUAL);

由于刷新是一个相当昂贵的操作(会话必须检查持久性上下文中每个实体的脏状态),因此,将刷新模式设置为 COMMIT 有时可以是一个有用的优化。但请注意——在此模式中,查询可能会返回陈旧的数据:

session.getTransaction().begin();
session.setFlushMode(FlushModeType.COMMIT); // disable AUTO-flush
session.persist(book);
var books =
        // flushing on query execution disabled
        session.createSelectionQuery("from Book")
                // no flush, query returns stale results
                .getResultList();
// flush occurs here, just before transaction commits
session.getTransaction().commit();

表 36. 刷新模式

Hibernate FlushMode

JPA FlushModeType

Interpretation

MANUAL

Never flush automatically

COMMIT

COMMIT

Flush before transaction commit

AUTO

AUTO

在事务提交之前刷新,并在执行查询之前刷新,查询结果可能会受到内存中修改的影响

ALWAYS

在事务提交之前刷新,并在执行每个查询之前刷新

减少刷新成本的第二种方法是在 read-only 模式下加载实体:

  1. Session.setDefaultReadOnly(false) 指定默认情况下应以只读模式加载由给定的会话加载的所有实体,

  2. SelectionQuery.setReadOnly(false) 指定由给定查询返回的每个实体都应以只读模式加载,且

  3. Session.setReadOnly(Object, false) 指定给定的实体已被会话加载,应切换为只读模式。

在只读模式下无需对实体实例执行脏检查。

5.9. Queries

Hibernate 提供了三种补充方式来编写查询:

  1. Hibernate Query Language,极具生命力的 JPQL 超集,它抽象了现代 SQL 方言的大多数功能,

  2. JPA criteria query API,包括扩展在内,它允许通过类型安全的 API 以编程方式构造几乎任何 HQL 查询,当然,

  3. 在所有其他方法都失败时,采用 native SQL 查询。

5.10. HQL queries

全面讨论查询语言所需的文本量几乎和本简介的其他部分一样多。幸运的是,HQL 已经在 A Guide to Hibernate Query Language 中得到了详尽(且详尽无遗)的描述。在此重复这些信息毫无意义。

在这里,我们想了解如何通过 SessionEntityManager API 执行查询。我们要调用的函数取决于查询的类型:

  1. selection queries 返回结果列表,但不修改数据,但

  2. mutation queries 修改数据,并返回已修改行的数量。

筛选查询通常以关键字 selectfrom 开头,而变更查询以 insertupdatedelete 开头。

表 37. 执行 HQL

Kind

Session method

EntityManager method

Query execution method

Selection

createSelectionQuery(String,Class)

createQuery(String,Class)

getResultList(), getSingleResult(), or getSingleResultOrNull()

Mutation

createMutationQuery(String)

createQuery(String)

executeUpdate()

因此,对于 Session API,我们将编写:

List<Book> matchingBooks =
        session.createSelectionQuery("from Book where title like :titleSearchPattern", Book.class)
            .setParameter("titleSearchPattern", titleSearchPattern)
            .getResultList();

或者,如果我们要坚持使用 JPA 标准 API:

List<Book> matchingBooks =
        entityManager.createQuery("select b from Book b where b.title like :titleSearchPattern", Book.class)
            .setParameter("titleSearchPattern", titleSearchPattern)
            .getResultList();

createSelectionQuery()createQuery() 之间的唯一区别是,如果 createSelectionQuery() 传入 insertdeleteupdate,它将抛出异常。

在上面的查询中,:titleSearchPattern 被称为 named parameter。我们还可以用数字来标识参数。这些数字被称为 ordinal parameters

List<Book> matchingBooks =
        session.createSelectionQuery("from Book where title like ?1", Book.class)
            .setParameter(1, titleSearchPattern)
            .getResultList();

当一个查询具有多个参数时,即使更冗长一些,命名参数也往往更容易阅读。

Never 将用户输入与 HQL 连接,并将连接字符串传递给 createSelectionQuery()。这将为攻击者在您的数据库服务器上执行任意代码提供可能性。

如果我们预期查询返回单个结果,可以使用 getSingleResult()

Book book =
        session.createSelectionQuery("from Book where isbn = ?1", Book.class)
            .setParameter(1, isbn)
            .getSingleResult();

或者,如果我们希望它至多返回一个结果,可以使用 getSingleResultOrNull()

Book bookOrNull =
        session.createSelectionQuery("from Book where isbn = ?1", Book.class)
            .setParameter(1, isbn)
            .getSingleResultOrNull();

当然,这其中的不同在于,如果数据库中没有匹配的行,getSingleResult() 将抛出异常,而 getSingleResultOrNull() 将仅返回 null

默认情况下,Hibernate 在执行查询之前会检查持久化上下文中是否存在脏实体,以确定是否应该刷新会话。如果存在与持久化上下文关联的多个实体,那么这可能是一项开销很大的操作。

要禁用此行为,可将刷新模式设置为 COMMITMANUAL

Book bookOrNull =
        session.createSelectionQuery("from Book where isbn = ?1", Book.class)
            .setParameter(1, isbn)
            .setHibernateFlushMode(MANUAL)
            .getSingleResult();

将刷新模式设置为 COMMITMANUAL 可能会导致查询返回过时结果。

有时候我们需要根据一系列可选条件在运行时构建一个查询。为此,JPA 提供了允许对查询进行编程构造的 API。

5.11. Criteria queries

设想一下,我们正在实现某种搜索屏幕,其中,系统用户可有多种不同的方式来限定查询结果集。例如,我们可以让他们通过标题和/或作者姓名来搜索书籍。当然,我们可以通过字符串连接来构建一个 HQL 查询,但这种方式有点脆弱,因此,有一个替代方案是件非常好的事情。

首先我们需要一个对象来构建条件查询。使用 JPA 标准 API,这将是一个 CriteriaBuilder,我们可以从 EntityManagerFactory 中获取它:

CriteriaBuilder builder = entityManagerFactory.getCriteriaBuilder();

但是,如果我们使用 SessionFactory ,我们会得到更好的内容 HibernateCriteriaBuilder

HibernateCriteriaBuilder builder = sessionFactory.getCriteriaBuilder();

HibernateCriteriaBuilder 扩展了 CriteriaBuilder,并添加了许多 JPQL 不具备的操作。

如果你正在使用 EntityManagerFactory ,不要绝望,你有两种很好的方法可以获取与该工厂关联的 HibernateCriteriaBuilder 。要么:

HibernateCriteriaBuilder builder = entityManagerFactory.unwrap(SessionFactory.class).getCriteriaBuilder(); HibernateCriteriaBuilder builder = entityManagerFactory.unwrap(SessionFactory.class).getCriteriaBuilder(); 或干脆:

HibernateCriteriaBuilder builder = (HibernateCriteriaBuilder) entityManagerFactory.getCriteriaBuilder(); HibernateCriteriaBuilder builder = (HibernateCriteriaBuilder) entityManagerFactory.getCriteriaBuilder();

我们准备创建一个条件查询。

CriteriaQuery<Book> query = builder.createQuery(Book.class);
Root<Book> book = query.from(Book.class);
Predicate where = builder.conjunction();
if (titlePattern != null) {
    where = builder.and(where, builder.like(book.get(Book_.title), titlePattern));
}
if (namePattern != null) {
    Join<Book,Author> author = book.join(Book_.author);
    where = builder.and(where, builder.like(author.get(Author_.name), namePattern));
}
query.select(book).where(where)
    .orderBy(builder.asc(book.get(Book_.title)));

这里,与之前一样,类 BookAuthor 由 Hibernate 的 JPA Metamodel Generator 生成。

请注意,我们不必将 titlePatternnamePattern 视为参数。这样做是安全的,因为默认情况下,Hibernate 自动而透明地将传递给 CriteriaBuilder 的字符串视为 JDBC 参数。

执行标准查询几乎与执行 HQL 完全相同。

表 38. 执行条件查询

Kind

Session method

EntityManager method

Query execution method

Selection

createSelectionQuery(CriteriaQuery)

createQuery(CriteriaQuery)

getResultList(), getSingleResult(), or getSingleResultOrNull()

Mutation

createMutationQuery(CriteriaUpdate) or createQuery(CriteriaDelete)

createQuery(CriteriaUpdate) or createQuery(CriteriaDelte)

executeUpdate()

例如:

List<Book> matchingBooks =
        session.createSelectionQuery(query)
            .getResultList();

更新、插入和删除查询类似地工作:

CriteriaDelete<Book> delete = builder.createCriteriaDelete(Book.class);
Root<Book> book = delete.from(Book.class);
delete.where(builder.lt(builder.year(book.get(Book_.publicationDate)), 2000));
session.createMutationQuery(delete).executeUpdate();

甚至可以将 HQL 查询字符串转换为 criteria 查询,并在执行前以编程方式修改查询:

HibernateCriteriaBuilder builder = sessionFactory.getCriteriaBuilder(); var query = builder.createQuery("from Book where year(publicationDate) > 2000", Book.class); var root = (Root<Book>) query.getRootList().get(0); query.where(builder.like(root.get(Book .title), builder.literal("Hibernate%")));query.orderBy(builder.asc(root.get(Book_.title)), builder.desc(root.get(Book_.isbn)));List<Book> matchingBooks = session.createSelectionQuery(query).getResultList();_ HibernateCriteriaBuilder builder = sessionFactory.getCriteriaBuilder(); var query = builder.createQuery("from Book where year(publicationDate) > 2000", Book.class); var root = (Root<Book>) query.getRootList().get(0); query.where(builder.like(root.get(Book .title), builder.literal("Hibernate%")));query.orderBy(builder.asc(root.get(Book_.title)), builder.desc(root.get(Book_.isbn)));List<Book> matchingBooks = session.createSelectionQuery(query).getResultList();_

您是否认为上面的一些代码过于冗长?我们觉得是。

5.12. A more comfortable way to write criteria queries

实际上,使 JPA 条件 API 比它应有的效率低的原因是需要将 CriteriaBuilder 的所有操作都作为实例方法来调用,而不是将它们作为 static 函数来使用。之所以会这样工作,是因为每个 JPA 提供程序都有自己的 CriteriaBuilder 实现。

Hibernate 6.3 引入了帮助类 CriteriaDefinition 以减少查询标准的冗余。我们的示例如下所示:

CriteriaQuery<Book> query =
        new CriteriaDefinition(entityManagerFactory, Book.class) {{
            select(book);
            if (titlePattern != null) {
                restrict(like(book.get(Book_.title), titlePattern));
            }
            if (namePattern != null) {
                var author = book.join(Book_.author);
                restrict(like(author.get(Author_.name), namePattern));
            }
            orderBy(asc(book.get(Book_.title)));
        }};

当所有其他方法都失败时,有时甚至在此之前,我们只剩下使用 SQL 编写查询这一选项。

5.13. Native SQL queries

HQL 是一种有助于减少 SQL 冗余的强大语言,并且显著提高了查询在数据库之间的可移植性。但最终,ORM 的真正价值不在于避免 SQL,而是在将 SQL 结果集返回到我们的 Java 程序后,减轻处理它们的痛苦。正如我们在 right up front 中所说,Hibernate 生成的 SQL 旨在与手写 SQL 结合使用,并且原生 SQL 查询是我们提供的用于简化此项工作的工具之一。

表 39. 执行 SQL

Kind

Session method

EntityManager method

Query execution method

Selection

createNativeQuery(String,Class)

createNativeQuery(String,Class)

getResultList(), getSingleResult(), or getSingleResultOrNull()

Mutation

createNativeMutationQuery(String)

createNativeQuery(String)

executeUpdate()

Stored procedure

createStoredProcedureCall(String)

createStoredProcedureQuery(String)

execute()

对于最简单的案例,Hibernate 可以推断出结果集的形状:

Book book =
        session.createNativeQuery("select * from Books where isbn = ?1", Book.class)
            .getSingleResult();

String title =
        session.createNativeQuery("select title from Books where isbn = ?1", String.class)
            .getSingleResult();

然而,通常情况下,JDBC ResultSetMetaData 中没有足够的信息来推断列到实体对象之间的映射。因此,对于更复杂的情况下,您需要使用 @SqlResultSetMapping 注释来定义一个已命名映射,然后将该名称传递给 createNativeQuery()。这会变得相当凌乱,所以我们不想通过向您展示一个示例来伤害您的眼睛。

默认情况下,Hibernate 不会在执行原生查询前刷新会话。这是因为会话不知道在内存中保存的哪些修改将影响查询结果。

因此,如果对 Book 进行任何未刷新的更改,此查询可能会返回陈旧数据:

List<Book> books =
        session.createNativeQuery("select * from Books", Book.class)
            .getResultList()

在此查询执行前,确保刷新持久性上下文有两种方法。

要么,我们可以简单地通过调用 flush() 或将刷新模式设为 ALWAYS 来强制刷新:

List<Book> books =
        session.createNativeQuery("select * from Books", Book.class)
            .setHibernateFlushMode(ALWAYS)
            .getResultList()

或者,我们还可以告诉 Hibernate 哪些已修改状态将影响查询结果:

List<Book> books =
        session.createNativeQuery("select * from Books", Book.class)
            .addSynchronizedEntityClass(Book.class)
            .getResultList()

可以使用 createStoredProcedureQuery()createStoredProcedureCall() 调用存储过程。

5.14. Limits, pagination, and ordering

如果查询可能返回我们一次性无法处理的多个结果,我们可以指定:

  1. limit 对返回的最大行数进行限制,且

  2. 可以选择 offset,它是按顺序返回结果集的第一行。

偏移量用于对查询结果进行分页。

有两种方法可以向 HQL 或本机 SQL 查询添加限制或偏移:

  1. 使用查询语言本身的语法,例如 offset 10 rows fetch next 20 rows only,或

  2. 使用 SelectionQuery 接口的方法 setFirstResult()setMaxResults()

如果 limit 或 offset 有参数,第二个选项就更简单了。例如,这样:

List<Book> books =
        session.createSelectionQuery("from Book where title like ?1 order by title", Book.class)
            .setParameter(1, titlePattern)
            .setMaxResults(MAX_RESULTS)
            .getResultList();

比这样更简单:

// a worse way to do pagination
List<Book> books =
        session.createSelectionQuery("from Book where title like ?1 order by title fetch first ?2 rows only", Book.class)
            .setParameter(1, titlePattern)
            .setParameter(2, MAX_RESULTS)
            .getResultList();

Hibernate 的 SelectionQuery 采用稍有不同的方法来分页查询结果:

List<Book> books =
        session.createSelectionQuery("from Book where title like ?1 order by title", Book.class)
            .setParameter(1, titlePattern)
            .setPage(Page.first(MAX_RESULTS))
            .getResultList();

getResultCount() 方法可用于显示结果的页数:

SelectionQuery<Book> query =
        session.createSelectionQuery("from Book where title like ?1 order by title", Book.class)
            .setParameter(1, titlePattern);
long pages = query.getResultCount() / MAX_RESULTS;
List<Book> books = query.setMaxResults(MAX_RESULTS).getResultList();

一个密切相关的问题是排序。分页通常与需要根据在运行时确定的字段对查询结果进行排序的需求结合在一起。因此,作为 HQL order by 子句的替代,SelectionQuery 提供按查询返回的实体类型的一个或多个字段对查询结果进行排序的能力:

List<Book> books =
        session.createSelectionQuery("from Book where title like ?1", Book.class)
            .setParameter(1, titlePattern)
            .setOrder(List.of(Order.asc(Book_.title), Order.asc(Book_.isbn)))
            .setMaxResults(MAX_RESULTS)
            .getResultList();

不幸的是,没有办法使用 JPA 的 TypedQuery 接口来执行此操作。

表 40. 查询限制、分页和排序的方法

Method name

Purpose

JPA-standard

setMaxResults()

设置查询返回的结果数上限

setFirstResult()

设置查询返回的结果偏移量

setPage()

通过指定 Page 对象设置上限和偏移量

setOrder()

指定如何对查询结果进行排序

getResultCount()

确定在不存在任何上限或偏移量的情况下查询将返回多少结果

我们刚刚看到的分页方法有时被称为 offset-based pagination。从 Hibernate 6.5 开始,有一种替代方法,它提供了一些优点,尽管使用起来有点困难。

5.15. Key-based pagination

Key-based pagination 旨在降低在页面请求间修改数据时漏掉或重复结果的可能性。可以通过一个示例轻松说明:

String QUERY = "from Book where publicationDate > :minDate";

// obtain the first page of results
KeyedResultList<Book> first =
        session.createSelectionQuery(QUERY, Book.class)
                .setParameter("minDate", minDate)
                .getKeyedResultList(Page.first(25)
                        .keyedBy(Order.asc(Book_.isbn)));
List<Book> firstPage = first.getResultList();
...

if (!firstPage.isLastPage()) {
    // obtain the second page of results
    KeyedResultList<Book> second =
            session.createSelectionQuery(QUERY, Book.class)
                    .setParameter("minDate", minDate))
                    .getKeyedResultList(firstPage.getNextPage());
    List<Book> secondPage = second.getResultList();
    ...
}

键式分页中的“键”指的是结果集的一个唯一键,它决定了查询结果的全部顺序。在这个示例中,Book.isbn 是键。

由于这段代码有点繁琐,所以基于键的分页最适合 generated query or finder methods

5.16. Representing projection lists

projection list_是一个查询返回的物品列表,即,_select_子句中的表达式列表。由于 Java 没有元组类型,因此,表示 Java 中的查询投影列表一直是 JPA 和 Hibernate 的问题。传统上,我们一直只是在大多数时候使用 _Object[]

var results =
        session.createSelectionQuery("select isbn, title from Book", Object[].class)
            .getResultList();

for (var result : results) {
    var isbn = (String) result[0];
    var title = (String) result[1];
    ...
}

这实际上有些难看。Java 的 record 类型现在提供了一个有趣的替代方案:

record IsbnTitle(String isbn, String title) {}

var results =
        session.createSelectionQuery("select isbn, title from Book", IsbnTitle.class)
            .getResultList();

for (var result : results) {
    var isbn = result.isbn();
    var title = result.title();
    ...
}

请注意,我们能够在执行查询的那一行之前声明 record

现在,这仅仅是 superficially 更安全,因为查询本身没有静态检查,因此我们不能说它客观地更好。但你可能觉得在美学上更令人愉悦。而如果我们要将查询结果传递到系统周围,那么使用 record 类型 much 更合适。

标准查询 API 为此问题提供了一个令人满意的解决方案。考虑以下代码:

var builder = sessionFactory.getCriteriaBuilder();
var query = builder.createTupleQuery();
var book = query.from(Book.class);
var bookTitle = book.get(Book_.title);
var bookIsbn = book.get(Book_.isbn);
var bookPrice = book.get(Book_.price);
query.select(builder.tuple(bookTitle, bookIsbn, bookPrice));
var resultList = session.createSelectionQuery(query).getResultList();
for (var result: resultList) {
    String title = result.get(bookTitle);
    String isbn = result.get(bookIsbn);
    BigDecimal price = result.get(bookPrice);
    ...
}

此代码明显是完全类型安全的,且远比我们使用 HQL 可能做到的好。

5.17. Named queries

@NamedQuery 批注让我们能够定义一个 HQL 查询,它作为引导流程的一部分进行编译和检查。这意味着我们可以更早地发现查询中的错误,而不是等到实际执行查询时。我们可以将 @NamedQuery 批注放在任何类上,甚至可以放在实体类上。

@NamedQuery(name="10BooksByTitle",
            query="from Book where title like :titlePattern order by title fetch first 10 rows only")
class BookQueries {}

我们必须确保带有 @NamedQuery 批注的类将被 Hibernate 扫描,方法如下:

  1. 通过将 &lt;class&gt;org.hibernate.example.BookQueries&lt;/class&gt; 添加到 persistence.xml,或

  2. by calling configuration.addClass(BookQueries.class).

不幸的是,JPA 的 @NamedQuery 注释不能放置在包描述符上。因此,Hibernate 提供了一个非常相似的注释, @org.hibernate.annotations.NamedQuery ,它 can 可以指定在包级别。如果我们在包级别声明一个命名查询,我们必须调用:

configuration.addPackage("org.hibernate.example") configuration.addPackage("org.hibernate.example") 以便 Hibernate 知道在哪里找到它。

@NamedNativeQuery 批注让我们对本机 SQL 查询执行相同的操作。使用 @NamedNativeQuery 的优势要小得多,这是因为 Hibernate 能够对以数据库的本机 SQL 方言编写的查询进行验证的方面很少。

表 41. 执行命名查询

Kind

Session method

EntityManager method

Query execution method

Selection

createNamedSelectionQuery(String,Class)

createNamedQuery(String,Class)

getResultList(), getSingleResult(), or getSingleResultOrNull()

Mutation

createNamedMutationQuery(String)

createNamedQuery(String)

executeUpdate()

我们像这样执行命名查询:

List<Book> books =
        entityManager.createNamedQuery(BookQueries_.QUERY_10_BOOKS_BY_TITLE)
            .setParameter("titlePattern", titlePattern)
            .getResultList()

这里,BookQueries.QUERY_10_BOOKS_BY_TITLE_ 是一个常量,值是 "10BooksByTitle",由元模型生成器生成。

请注意,执行命名查询的代码不知道查询是使用 HQL 编写还是使用本机 SQL 编写,这使得稍后更改和优化查询稍稍轻松一些。

在启动时检查我们的查询很好。在编译时对其进行检查会更好。在 Organizing persistence logic 中,我们提到 Metamodel Generator 可以借助 @CheckHQL 注释为我们完成此操作,我们将其作为使用 @NamedQuery 的理由。

但实际上,Hibernate 有一个单独的 Query Validator ,它能够对作为 createQuery() 及其关联项的参数出现的 HQL 查询字符串执行编译时验证。如果我们使用 Query 验证器,则使用命名查询就没有多大优势。

5.18. Controlling lookup by id

我们可以通过 HQL、条件或原生 SQL 查询完成几乎任何事情。但是,当我们已经知道所需的实体标识符时,查询就像杀鸡用牛刀。并且查询不会有效地使用 second level cache

我们早先遇到了 find() 方法。这是通过 id 执行 lookup 的最基本方法。但我们也 already saw 到,它不能完全完成所有操作。因此,Hibernate 有些 API 可以简化某些更复杂的查找:

表 42. 按 ID 查找的操作

Method name

Purpose

byId()

让我们指定关联提取通过一个 EntityGraph,就如我们所见;还可以让我们指定一些其他选项,包括查找 interacts with the second level cache的方式,以及实体是否应加载在只读模式下

byMultipleIds()

它允许我们同时加载 batch 个 ID

当我们需要按 id 检索同一个实体类的多个实例时,批量加载非常有用:

var graph = session.createEntityGraph(Book.class);
graph.addSubgraph(Book_.publisher);

List<Book> books =
        session.byMultipleIds(Book.class)
            .withFetchGraph(graph)  // control association fetching
            .withBatchSize(20)      // specify an explicit batch size
            .with(CacheMode.GET)    // control interaction with the cache
            .multiLoad(bookIds);

给定的 bookIds 列表将被分成多个批次,而且每个批次将从数据库中通过单个 select 提取。如果我们没有明确指定批次大小,那么将自动选择一个批次大小。

我们还有一些操作用于处理通过 natural id 进行的查找:

Method name

Purpose

bySimpleNaturalId()

对于仅一个属性的实体,用 @NaturalId 进行注释

byNaturalId()

对于带有多个属性的实体进行了注释 @NaturalId

byMultipleNaturalId()

让我们加载一个 batch 自然 id

以下是使用其复合自然标识符检索实体的方式:

Book book =
        session.byNaturalId(Book.class)
            .using(Book_.isbn, isbn)
            .using(Book_.printing, printing)
            .load();

请注意,此代码片段是完全类型安全的,这同样要归功于 Metamodel Generator

5.19. Interacting directly with JDBC

我们时不时会遇到需要编写可直接调用 JDBC 的代码。不幸的是,JPA 没有提供一种好的方法来做到这一点,但是 Hibernate Session 可以。

session.doWork(connection -> {
    try (var callable = connection.prepareCall("{call myproc(?)}")) {
        callable.setLong(1, argument);
        callable.execute();
    }
});

传递给作业的 Connection 与会话使用的连接相同,因此使用该连接执行的任何作业都会在同一事务上下文中发生。

如果作业返回一个值,请使用 doReturningWork() 而不是 doWork()

在事务和数据库连接由容器管理的容器环境中,这可能不是获取 JDBC 连接的最简单方法。

5.20. What to do when things go wrong

对象/关系映射被称为“计算机科学界的越南”。提出这一比喻的那个人是美国人,因此有人猜测他想暗示某种不可取胜的战争。这相当具有讽刺意味,因为就在他发表这一评论时,Hibernate 已经接近赢得这场战争。

如今,越南已经是一个和平的国家,人均 GDP 飞速增长,而 ORM 则是一个已解决的问题。话虽如此,但 Hibernate 还是比较复杂的,而且对于没有经验的人(甚至偶尔也会让有经验的人)来说,ORM 仍然存在着许多缺陷。有时候会出问题。

在本节中,我们将简要概述一些避免陷入“泥潭”的一般策略。

  1. 理解 SQL 和关系模型。了解 RDBMS 的功能。如果你足够幸运地拥有 DBA,那就与他紧密合作。Hibernate 不是 Java 对象的“透明持久性”。它能让两项出色的技术顺利协同工作。

  2. 由 Hibernate 执行 Log the SQL。在你实际检查正在执行的 SQL 之前,你无法知道你的持久性逻辑是正确的。即使一切都似乎“正常”,也可能潜伏着 N+1 selects monster

  3. modifying bidirectional associations 时要小心。原则上,你应该更新关联的 both ends。但 Hibernate 并不严格执行此操作,因为在某些情况下,这种规则过于严格。无论如何,由你决定维护整个模型的一致性。

  4. 切勿在各个线程或并发事务中 leak a persistence context。必须有策略或框架来确保永远不会发生这种情况。

  5. 在运行返回结果集较大的查询时,要注意 session cache 的大小。考虑使用 stateless session

  6. 仔细考虑 second-level cache 的语义,以及缓存策略如何影响事务隔离。

  7. 避免使用你不需要的花里胡哨的东西。Hibernate 提供非常丰富的功能,这是一件好事,因为它满足了众多用户(其中许多用户有几个非常特殊的需求)。但没有用户 all 有那些特殊需求。在所有概率中,你没有一个。以最合理的方式编写你的领域模型,使用最合理的映射策略。

  8. 当某些事物未按预期工作时,simplify。隔离问题。找到可复现此行为的最小测试用例,before 在线寻求帮助。在大多数情况下,仅隔离问题这一行为就会提示一个明显的解决方案。

  9. 避免“包装” JPA 的框架和库。如果说 Hibernate 和 ORM 有什么批评之处,有时候确实如此:does 它让你不能直接控制 JDBC。另一层只会让你更远离。

  10. 避免从随机博主或 Stackoverflow 回复者那里复制/粘贴代码。你在线找到的许多建议都不是最简单的解决方案,而许多建议也不适用于 Hibernate 6。相反,understand 你在做什么;研究你使用的 API 的 Javadoc;阅读 JPA 规范;遵循我们在这份文档中给出的建议;到 Zulip 上直接找到 Hibernate 团队。(当然,我们有时会有点乖戾,但我们_do_ 总是希望你成功。)

  11. 始终考虑其他选项。你不需要为 everything 使用 Hibernate。