Hibernate ORM 中文操作指南

5. Interacting with the database

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

To interact with the database, that is, to execute queries, or to insert, update, or delete data, we need an instance of one of the following objects:

  1. a JPA EntityManager,

  2. a Hibernate Session, or

  3. a Hibernate StatelessSession.

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

The Session interface extends EntityManager, and so the only difference between the two interfaces is that Session offers a few more operations.

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

Actually, in Hibernate, every EntityManager is a Session, and you can narrow it like this:

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

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

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

An instance of Session (or of EntityManager) is a stateful session. It mediates the interaction between your program and the database via a operations on a persistence context.

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

In this chapter, we’re not going to talk much about StatelessSession. We’ll come back to this very useful API when we talk about performance. What you need to know for now is that a stateless session doesn’t have a persistence context.

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

Still, we should let you know that some people prefer to use StatelessSession everywhere. It’s a simpler programming model, and lets the developer interact with the database more directly.

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

Stateful sessions certainly have their advantages, but they’re more difficult to reason about, and when something goes wrong, the error messages can be more difficult to understand.

5.1. Persistence Contexts

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

A persistence context is a sort of cache; we sometimes call it the "first-level cache", to distinguish it from the second-level cache. For every entity instance read from the database within the scope of a persistence context, and for every new entity made persistent within the scope of the persistence context, the context holds a unique mapping from the identifier of the entity instance to the instance itself.

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

Thus, an entity instance may be in one of three states with respect to a given persistence context:

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

  • persistent — currently associated with the persistence context, or

  • detached — previously persistent in another session, but not currently associated with this persistence context.

entity lifecyle

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

At any given moment, an instance may be associated with at most one persistence context.

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

The lifetime of a persistence context usually corresponds to the lifetime of a transaction, though it’s possible to have a persistence context that spans several database-level transactions that form a single logical unit of work.

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

A persistence context—that is, a Session or EntityManager—absolutely positively must not be shared between multiple threads or between concurrent transactions.

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

If you accidentally leak a session across threads, you will suffer.

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

There are several reasons we like persistence contexts.

  • They help avoid data aliasing: if we modify an entity in one section of code, then other code executing within the same persistence context will see our modification.

  • They enable automatic dirty checking: after modifying an entity, we don’t need to perform any explicit operation to ask Hibernate to propagate that change back to the database. Instead, the change will be automatically synchronized with the database when the session is flushed.

  • They can improve performance by avoiding a trip to the database when a given entity instance is requested repeatedly in a given unit of work.

  • They make it possible to transparently batch together multiple database operations.

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

A persistence context also allows us to detect circularities when performing operations on graphs of entities. (Even in a stateless session, we need some sort of temporary cache of the entity instances we’ve visited while executing a query.)

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

On the other hand, stateful sessions come with some very important restrictions, since:

  1. persistence contexts aren’t threadsafe, and can’t be shared across threads, and

  2. a persistence context can’t be reused across unrelated transactions, since that would break the isolation and atomicity of the transactions.

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

Furthermore, a persistence context holds a hard references to all its entities, preventing them from being garbage collected. Thus, the session must be discarded once a unit of work is complete.

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

If you don’t completely understand the previous passage, go back and re-read it until you do. A great deal of human suffering has resulted from users mismanaging the lifecycle of the Hibernate Session or JPA EntityManager.

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

We’ll conclude by noting that whether a persistence context helps or harms the performance of a given unit of work depends greatly on the nature of the unit of work. For this reason Hibernate provides both stateful and stateless sessions.

5.2. Creating a session

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

Sticking with standard JPA-defined APIs, we saw how to obtain an EntityManagerFactory in Configuration using JPA XML. It’s quite unsurprising that we may use this object to create an EntityManager:

EntityManager entityManager = entityManagerFactory.createEntityManager();

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

When we’re finished with the EntityManager, we should explicitly clean it up:

entityManager.close();

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

On the other hand, if we’re starting from a SessionFactory, as described in Configuration using Hibernate API, we may use:

Session session = sessionFactory.openSession();

但我们仍需要清理:

But we still need to clean up:

session.close();

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

Outside a container environment, we’ll also have to write code to manage database transactions.

5.3. Managing transactions

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

Using JPA-standard APIs, the EntityTransaction interface allows us to control database transactions. The idiom we recommend is the following:

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,我们可以编写一些非常相似的代码,但由于此类代码非常繁琐,我们有一个更好的选择:

Using Hibernate’s native APIs we might write something really similar, but since this sort of code is extremely tedious, we have a much nicer option:

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

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

JPA doesn’t have a standard way to set the transaction timeout, but Hibernate does:

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

5.4. Operations on the persistence context

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

Of course, the main reason we need an EntityManager is to do stuff to the database. The following important operations let us interact with the persistence context and schedule modifications to the data:

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

Table 34. Methods for modifying data and managing the persistence context

Method name and parameters

Effect

persist(Object)

Make a transient object persistent and schedule a SQL insert statement for later execution

remove(Object)

Make a persistent object transient and schedule a SQL delete statement for later execution

merge(Object)

Copy the state of a given detached object to a corresponding managed persistent instance and return the persistent object

detach(Object)

Disassociate a persistent object from a session without affecting the database

clear()

Empty the persistence context and detach all its entities

flush()

Detect changes made to persistent objects association with the session and synchronize the database state with the state of the session by executing SQL insert, update, and delete statements

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

Notice that persist() and remove() have no immediate effect on the database, and instead simply schedule a command for later execution. Also notice that there’s no update() operation for a stateful session. Modifications are automatically detected when the session is flushed.

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

On the other hand, except for getReference(), the following operations all result in immediate access to the database:

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

Table 35. Methods for reading and locking data

Method name and parameters

Effect

find(Class,Object)

Obtain a persistent object given its type and its id

find(Class,Object,LockModeType)

Obtain a persistent object given its type and its id, requesting the given optimistic or pessimistic lock mode

getReference(Class,id)

Obtain a reference to a persistent object given its type and its id, without actually loading its state from the database

getReference(Object)

Obtain a reference to a persistent object with the same identity as the given detached instance, without actually loading its state from the database

refresh(Object)

Refresh the persistent state of an object using a new SQL select to retrieve its current state from the database

refresh(Object,LockModeType)

Refresh the persistent state of an object using a new SQL select to retrieve its current state from the database, requesting the given optimistic or pessimistic lock mode

lock(Object, LockModeType)

Obtain an optimistic or pessimistic lock on a persistent object

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

Any of these operations might throw an exception. Now, if an exception occurs while interacting with the database, there’s no good way to resynchronize the state of the current persistence context with the state held in database tables.

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

Therefore, a session is considered to be unusable after any of its methods throws an exception.

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

The persistence context is fragile. If you receive an exception from Hibernate, you should immediately close and discard the current session. Open a new session if you need to, but throw the bad one away first.

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

Each of the operations we’ve seen so far affects a single entity instance passed as an argument. But there’s a way to set things up so that an operation will propagate to associated entities.

5.5. Cascading persistence operations

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

It’s quite often the case that the lifecycle of a child entity is completely dependent on the lifecycle of some parent. This is especially common for many-to-one and one-to-one associations, though it’s very rare for many-to-many associations.

例如,建立一个 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 类型关系的情况非常普遍。

For example, it’s quite common to make an Order and all its Item_s persistent in the same transaction, or to delete a _Project and its Files_s at once. This sort of relationship is sometimes called a _whole/part-type relationship.

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

Cascading is a convenience which allows us to propagate one of the operations listed in Operations on the persistence context from a parent to its children. To set up cascading, we specify the cascade member of one of the association mapping annotations, usually @OneToMany or @OneToOne.

@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 应该自动删除。

Orphan removal indicates that an Item should be automatically deleted if it is removed from the set of items belonging to its parent Order.

5.6. Proxies and lazy fetching

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

Our data model is a set of interconnected entities, and in Java our whole dataset would be represented as an enormous interconnected graph of objects. It’s possible that this graph is disconnected, but more likely it’s connected, or composed of a relatively small number of connected subgraphs.

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

Therefore, when we retrieve on object belonging to this graph from the database and instantiate it in memory, we simply can’t recursively retrieve and instantiate all its associated entities. Quite aside from the waste of memory on the VM side, this process would involve a huge number of round trips to the database server, or a massive multidimensional cartesian product of tables, or both. Instead, we’re forced to cut the graph somewhere.

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

Hibernate solves this problem using proxies and lazy fetching. A proxy is an object that masquerades as a real entity or collection, but doesn’t actually hold any state, because that state has not yet been fetched from the database. When you call a method of the proxy, Hibernate will detect the call and fetch the state from the database before allowing the invocation to proceed to the real entity object or collection.

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

Now for the gotchas:

  • Hibernate will only do this for an entity which is currently associated with a persistence context. Once the session ends, and the persistence context is cleaned up, the proxy is no longer fetchable, and instead its methods throw the hated LazyInitializationException.

  • For a polymorphic association, Hibernate does not know the concrete type of the referenced entity when the proxy is instantiated, and so operations like instanceof and typecasts do not work correctly when applied to a proxy.

  • A round trip to the database to fetch the state of a single entity instance is just about the least efficient way to access data. It almost inevitably leads to the infamous N+1 selects problem we’ll discuss later when we talk about how to optimize association fetching.

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

The @ConcreteProxy annotation solves gotcha 2, but at the cost of performance (extra joins), and so its use is not generally recommended, except in very special circumstances.

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

We’re getting a bit ahead of ourselves here, but let’s quickly mention the general strategy we recommend to navigate past these gotchas:

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

All associations should be set fetch=LAZY to avoid fetching extra data when it’s not needed. As we mentioned earlier, this setting is not the default for @ManyToOne associations, and must be specified explicitly.

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

But strive to avoid writing code which triggers lazy fetching. Instead, fetch all the data you’ll need upfront at the beginning of a unit of work, using one of the techniques described in Association fetching, usually, using join fetch in HQL or an EntityGraph.

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

It’s important to know that some operations which may be performed with an unfetched proxy don’t require fetching its state from the database. First, we’re always allowed to obtain its identifier:

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

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

Second, we may create an association to a proxy:

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

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

Sometimes it’s useful to test whether a proxy or collection has been fetched from the database. JPA lets us do this using the PersistenceUnitUtil:

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

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

Hibernate has a slightly easier way to do it:

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

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

But the static methods of the Hibernate class let us do a lot more, and it’s worth getting a bit familiar with them.

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

Of particular interest are the operations which let us work with unfetched collections without fetching their state from the database. For example, consider this code:

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 都留作未提取状态。

This code fragment leaves both the set book.authors and the proxy authorRef unfetched.

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

Finally, Hibernate.initialize() is a convenience method that force-fetches a proxy or collection:

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

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

But of course, this code is very inefficient, requiring two trips to the database to obtain data that could in principle be retrieved with just one query.

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

It’s clear from the discussion above that we need a way to request that an association be eagerly fetched using a database join, thus protecting ourselves from the infamous N+1 selects. One way to do this is by passing an EntityGraph to find().

5.7. Entity graphs and eager fetching

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

When an association is mapped fetch=LAZY, it won’t, by default, be fetched when we call the find() method. We may request that an association be fetched eagerly (immediately) by passing an EntityGraph to find().

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

The JPA-standard API for this is a bit unwieldy:

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 有更好的方法:

This is untypesafe and unnecessarily verbose. Hibernate has a better way:

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

This code adds a left outer join to our SQL query, fetching the associated Publisher along with the Book.

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

We may even attach additional nodes to our 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 查询。

This results in a SQL query with four _left outer join_s.

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

In the code examples above, The classes Book and Author are generated by the JPA Metamodel Generator we saw earlier. They let us refer to attributes of our model in a completely type-safe way. We’ll use them again, below, when we talk about Criteria queries.

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

JPA specifies that any given EntityGraph may be interpreted in two different ways.

  1. A fetch graph specifies exactly the associations that should be eagerly loaded. Any association not belonging to the entity graph is proxied and loaded lazily only if required.

  2. A load graph specifies that the associations in the entity graph are to be fetched in addition to the associations mapped fetch=EAGER.

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

You’re right, the names make no sense. But don’t worry, if you take our advice, and map your associations fetch=LAZY, there’s no difference between a "fetch" graph and a "load" graph, so the names don’t matter.

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

JPA even specifies a way to define named entity graphs using annotations. But the annotation-based API is so verbose that it’s just not worth using.

5.8. Flushing the session

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

From time to time, a flush operation is triggered, and the session synchronizes dirty state held in memory—that is, modifications to the state of entities associated with the persistence context—with persistent state held in the database. Of course, it does this by executing SQL INSERT, UPDATE, and DELETE statements.

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

By default, a flush is triggered:

  1. when the current transaction commits, for example, when Transaction.commit() is called,

  2. before execution of a query whose result would be affected by the synchronization of dirty state held in memory, or

  3. when the program directly calls flush().

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

In the following code, the flush occurs when the transaction commits:

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();

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

But in this code, the flush occurs when the query is executed:

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()_:

It’s always possible to call flush() explicitly:

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 允许这样做。

Notice that SQL statements are not usually executed synchronously by methods of the Session interface like persist() and remove(). If synchronous execution of SQL is desired, the StatelessSession allows this.

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

This behavior can be controlled by explicitly setting the flush mode. For example, to disable flushes that occur before query execution, call:

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

Hibernate allows greater control over the flush mode than JPA:

session.setHibernateFlushMode(FlushMode.MANUAL);

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

Since flushing is a somewhat expensive operation (the session must dirty-check every entity in the persistence context), setting the flush mode to COMMIT can occasionally be a useful optimization. But take care—​in this mode, queries might return stale data:

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. 刷新模式

Table 36. Flush modes

Hibernate FlushMode

JPA FlushModeType

Interpretation

MANUAL

Never flush automatically

COMMIT

COMMIT

Flush before transaction commit

AUTO

AUTO

Flush before transaction commit, and before execution of a query whose results might be affected by modifications held in memory

ALWAYS

Flush before transaction commit, and before execution of every query

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

A second way to reduce the cost of flushing is to load entities in read-only mode:

  1. Session.setDefaultReadOnly(false) specifies that all entities loaded by a given session should be loaded in read-only mode by default,

  2. SelectionQuery.setReadOnly(false) specifies that every entity returned by a given query should be loaded in read-only mode, and

  3. Session.setReadOnly(Object, false) specifies that a given entity already loaded by the session should be switched to read-only mode.

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

It’s not necessary to dirty-check an entity instance in read-only mode.

5.9. Queries

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

Hibernate features three complementary ways to write queries:

  1. the Hibernate Query Language, an extremely powerful superset of JPQL, which abstracts most of the features of modern dialects of SQL,

  2. the JPA criteria query API, along with extensions, allowing almost any HQL query to be constructed programmatically via a typesafe API, and, of course

  3. for when all else fails, native SQL queries.

5.10. HQL queries

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

A full discussion of the query language would require almost as much text as the rest of this Introduction. Fortunately, HQL is already described in exhaustive (and exhausting) detail in A Guide to Hibernate Query Language. It doesn’t make sense to repeat that information here.

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

Here we want to see how to execute a query via the Session or EntityManager API. The method we call depends on what kind of query it is:

  1. selection queries return a result list, but do not modify the data, but

  2. mutation queries modify data, and return the number of modified rows.

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

Selection queries usually start with the keyword select or from, whereas mutation queries begin with the keyword insert, update, or delete.

表 37. 执行 HQL

Table 37. Executing 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,我们将编写:

So for the Session API we would write:

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

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

Or, if we’re sticking to the JPA-standard APIs:

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,它将抛出异常。

The only difference between createSelectionQuery() and createQuery() is that createSelectionQuery() throws an exception if passed an insert, delete, or update.

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

In the query above, :titleSearchPattern is called a named parameter. We may also identify parameters by a number. These are called ordinal parameters.

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

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

When a query has multiple parameters, named parameters tend to be easier to read, even if slightly more verbose.

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

Never concatenate user input with HQL and pass the concatenated string to createSelectionQuery(). This would open up the possibility for an attacker to execute arbitrary code on your database server.

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

If we’re expecting a query to return a single result, we can use getSingleResult().

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

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

Or, if we’re expecting it to return at most one result, we can use getSingleResultOrNull().

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

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

The difference, of course, is that getSingleResult() throws an exception if there’s no matching row in the database, whereas getSingleResultOrNull() just returns null.

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

By default, Hibernate dirty checks entities in the persistence context before executing a query, in order to determine if the session should be flushed. If there are many entities association with the persistence context, then this can be an expensive operation.

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

To disable this behavior, set the flush mode to COMMIT or MANUAL:

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

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

Setting the flush mode to COMMIT or MANUAL might cause the query to return stale results.

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

Occasionally we need to build a query at runtime, from a set of optional conditions. For this, JPA offers an API which allows programmatic construction of a query.

5.11. Criteria queries

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

Imagine we’re implementing some sort of search screen, where the user of our system is offered several different ways to constrain the query result set. For example, we might let them search for books by title and/or the author name. Of course, we could construct a HQL query by string concatenation, but this is a bit fragile, so it’s quite nice to have an alternative.

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

First we need an object for building criteria queries. Using the JPA-standard APIs, this would be a CriteriaBuilder, and we get it from the EntityManagerFactory:

CriteriaBuilder builder = entityManagerFactory.getCriteriaBuilder();

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

But if we have a SessionFactory, we get something much better, a HibernateCriteriaBuilder:

HibernateCriteriaBuilder builder = sessionFactory.getCriteriaBuilder();

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

The HibernateCriteriaBuilder extends CriteriaBuilder and adds many operations that JPQL doesn’t have.

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

If you’re using EntityManagerFactory, don’t despair, you have two perfectly good ways to obtain the HibernateCriteriaBuilder associated with that factory. Either:

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

HibernateCriteriaBuilder builder = entityManagerFactory.unwrap(SessionFactory.class).getCriteriaBuilder(); HibernateCriteriaBuilder builder = entityManagerFactory.unwrap(SessionFactory.class).getCriteriaBuilder(); Or simply:

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

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

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

We’re ready to create a criteria query.

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 生成。

Here, as before, the classes Book and Author are generated by Hibernate’s JPA Metamodel Generator.

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

Notice that we didn’t bother treating titlePattern and namePattern as parameters. That’s safe because, by default, Hibernate automatically and transparently treats strings passed to the CriteriaBuilder as JDBC parameters.

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

Execution of a criteria query works almost exactly like execution of HQL.

表 38. 执行条件查询

Table 38. Executing criteria queries

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()

例如:

For example:

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

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

Update, insert, and delete queries work similarly:

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 查询,并在执行前以编程方式修改查询:

It’s even possible to transform a HQL query string to a criteria query, and modify the query programmatically before execution:

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();_

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();_

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

Do you find some of the code above a bit too verbose? We do.

5.12. A more comfortable way to write criteria queries

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

Actually, what makes the JPA criteria API less ergonomic than it should be is the need to call all operations of the CriteriaBuilder as instance methods, instead of having them as static functions. The reason it works this way is that each JPA provider has its own implementation of CriteriaBuilder.

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

Hibernate 6.3 introduces the helper class CriteriaDefinition to reduce the verbosity of criteria queries. Our example looks like this:

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 编写查询这一选项。

When all else fails, and sometimes even before that, we’re left with the option of writing a query in SQL.

5.13. Native SQL queries

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

HQL is a powerful language which helps reduce the verbosity of SQL, and significantly increases portability of queries between databases. But ultimately, the true value of ORM is not in avoiding SQL, but in alleviating the pain involved in dealing with SQL result sets once we get them back to our Java program. As we said right up front, Hibernate’s generated SQL is meant to be used in conjunction with handwritten SQL, and native SQL queries are one of the facilities we provide to make that easy.

表 39. 执行 SQL

Table 39. Executing 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 可以推断出结果集的形状:

For the most simple cases, Hibernate can infer the shape of the result set:

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()。这会变得相当凌乱,所以我们不想通过向您展示一个示例来伤害您的眼睛。

However, in general, there isn’t enough information in the JDBC ResultSetMetaData to infer the mapping of columns to entity objects. So for more complicated cases, you’ll need to use the @SqlResultSetMapping annotation to define a named mapping, and pass the name to createNativeQuery(). This gets fairly messy, so we don’t want to hurt your eyes by showing you an example of it.

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

By default, Hibernate doesn’t flush the session before execution of a native query. That’s because the session is unaware of which modifications held in memory would affect the results of the query.

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

So if there are any unflushed changes to _Book_s, this query might return stale data:

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

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

There’s two ways to ensure the persistence context is flushed before this query is executed.

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

Either, we could simply force a flush by calling flush() or by setting the flush mode to ALWAYS:

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

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

Or, alternatively, we could tell Hibernate which modified state affects the results of the query:

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

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

You can call stored procedures using createStoredProcedureQuery() or createStoredProcedureCall().

5.14. Limits, pagination, and ordering

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

If a query might return more results than we can handle at one time, we may specify:

  1. a limit on the maximum number of rows returned, and,

  2. optionally, an offset, the first row of an ordered result set to return.

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

The offset is used to paginate query results.

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

There’s two ways to add a limit or offset to a HQL or native SQL query:

  1. using the syntax of the query language itself, for example, offset 10 rows fetch next 20 rows only, or

  2. using the methods setFirstResult() and setMaxResults() of the SelectionQuery interface.

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

If the limit or offset is parameterized, the second option is simpler. For example, this:

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

比这样更简单:

is simpler than:

// 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 采用稍有不同的方法来分页查询结果:

Hibernate’s SelectionQuery has a slightly different way to paginate the query results:

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() 方法可用于显示结果的页数:

The getResultCount() method is useful for displaying the number of pages of results:

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 提供按查询返回的实体类型的一个或多个字段对查询结果进行排序的能力:

A closely-related issue is ordering. It’s quite common for pagination to be combined with the need to order query results by a field that’s determined at runtime. So, as an alternative to the HQL order by clause, SelectionQuery offers the ability to specify that the query results should be ordered by one or more fields of the entity type returned by the query:

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 接口来执行此操作。

Unfortunately, there’s no way to do this using JPA’s TypedQuery interface.

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

Table 40. Methods for query limits, pagination, and ordering

Method name

Purpose

JPA-standard

setMaxResults()

Set a limit on the number of results returned by a query

setFirstResult()

Set an offset on the results returned by a query

setPage()

Set the limit and offset by specifying a Page object

setOrder()

Specify how the query results should be ordered

getResultCount()

Determine how many results the query would return in the absence of any limit or offset

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

The approach to pagination we’ve just seen is sometimes called offset-based pagination. Since Hibernate 6.5, there’s an alternative approach, which offers some advantages, though it’s a little more difficult to use.

5.15. Key-based pagination

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

Key-based pagination aims to reduce the likelihood of missed or duplicate results when data is modified between page requests. It’s most easily illustrated with an example:

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 是键。

The "key" in key-based pagination refers to a unique key of the result set which determines a total order on the query results. In this example, Book.isbn is the key.

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

Since this code is a little bit fiddly, key-based pagination works best with generated query or finder methods.

5.16. Representing projection lists

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

A projection list is the list of things that a query returns, that is, the list of expressions in the select clause. Since Java has no tuple types, representing query projection lists in Java has always been a problem for JPA and Hibernate. Traditionally, we’ve just used Object[] most of the time:

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 类型现在提供了一个有趣的替代方案:

This is really a bit ugly. Java’s record types now offer an interesting alternative:

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

Notice that we’re able to declare the record right before the line which executes the query.

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

Now, this is only superficially more typesafe, since the query itself is not checked statically, and so we can’t say it’s objectively better. But perhaps you find it more aesthetically pleasing. And if we’re going to be passing query results around the system, the use of a record type is much better.

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

The criteria query API offers a much more satisfying solution to the problem. Consider the following code:

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 可能做到的好。

This code is manifestly completely typesafe, and much better than we can hope to do with HQL.

5.17. Named queries

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

The @NamedQuery annotation lets us define a HQL query that is compiled and checked as part of the bootstrap process. This means we find out about errors in our queries earlier, instead of waiting until the query is actually executed. We can place the @NamedQuery annotation on any class, even on an entity class.

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

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

We have to make sure that the class with the @NamedQuery annotation will be scanned by Hibernate, either:

  1. by adding <class>org.hibernate.example.BookQueries</class> to persistence.xml, or

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

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

Unfortunately, JPA’s @NamedQuery annotation can’t be placed on a package descriptor. Therefore, Hibernate provides a very similar annotation, @org.hibernate.annotations.NamedQuery which can be specified at the package level. If we declare a named query at the package level, we must call:

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

configuration.addPackage("org.hibernate.example") configuration.addPackage("org.hibernate.example") so that Hibernate knows where to find it.

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

The @NamedNativeQuery annotation lets us do the same for native SQL queries. There’s much less advantage to using @NamedNativeQuery, because there is very little that Hibernate can do to validate the correctness of a query written in the native SQL dialect of your database.

表 41. 执行命名查询

Table 41. Executing named queries

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()

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

We execute our named query like this:

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

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

Here, BookQueries.QUERY_10_BOOKS_BY_TITLE_ is a constant with value "10BooksByTitle", generated by the Metamodel Generator.

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

Note that the code which executes the named query is not aware of whether the query was written in HQL or in native SQL, making it slightly easier to change and optimize the query later.

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

It’s nice to have our queries checked at startup time. It’s even better to have them checked at compile time. In Organizing persistence logic, we mentioned that the Metamodel Generator can do that for us, with the help of the @CheckHQL annotation, and we presented that as a reason to use @NamedQuery.

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

But actually, Hibernate has a separate Query Validator capable of performing compile-time validation of HQL query strings that occur as arguments to createQuery() and friends. If we use the Query Validator, there’s not much advantage to the use of named queries.

5.18. Controlling lookup by id

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

We can do almost anything via HQL, criteria, or native SQL queries. But when we already know the identifier of the entity we need, a query can feel like overkill. And queries don’t make efficient use of the second level cache.

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

We met the find() method earlier. It’s the most basic way to perform a lookup by id. But as we also already saw, it can’t quite do everything. Therefore, Hibernate has some APIs that streamline certain more complicated lookups:

表 42. 按 ID 查找的操作

Table 42. Operations for lookup by id

Method name

Purpose

byId()

Lets us specify association fetching via an EntityGraph, as we saw; also lets us specify some additional options, including how the lookup interacts with the second level cache, and whether the entity should be loaded in read-only mode

byMultipleIds()

Lets us load a batch of ids at the same time

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

Batch loading is very useful when we need to retrieve multiple instances of the same entity class by 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 提取。如果我们没有明确指定批次大小,那么将自动选择一个批次大小。

The given list of bookIds will be broken into batches, and each batch will be fetched from the database in a single select. If we don’t specify the batch size explicitly, a batch size will be chosen automatically.

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

We also have some operations for working with lookups by natural id:

Method name

Purpose

bySimpleNaturalId()

For an entity with just one attribute is annotated @NaturalId

byNaturalId()

For an entity with multiple attributes are annotated @NaturalId

byMultipleNaturalId()

Lets us load a batch of natural ids at the same time

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

Here’s how we can retrieve an entity by its composite natural id:

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

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

Notice that this code fragment is completely typesafe, again thanks to the Metamodel Generator.

5.19. Interacting directly with JDBC

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

From time to time we run into the need to write some code that calls JDBC directly. Unfortunately, JPA offers no good way to do this, but the Hibernate Session does.

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

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

The Connection passed to the work is the same connection being used by the session, and so any work performed using that connection occurs in the same transaction context.

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

If the work returns a value, use doReturningWork() instead of doWork().

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

In a container environment where transactions and database connections are managed by the container, this might not be the easiest way to obtain the JDBC connection.

5.20. What to do when things go wrong

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

Object/relational mapping has been called the "Vietnam of computer science". The person who made this analogy is American, and so one supposes that he meant to imply some kind of unwinnable war. This is quite ironic, since at the very moment he made this comment, Hibernate was already on the brink of winning the war.

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

Today, Vietnam is a peaceful country with exploding per-capita GDP, and ORM is a solved problem. That said, Hibernate is complex, and ORM still presents many pitfalls for the inexperienced, even occasionally for the experienced. Sometimes things go wrong.

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

In this section we’ll quickly sketch some general strategies for avoiding "quagmires".

  1. Understand SQL and the relational model. Know the capabilities of your RDBMS. Work closely with the DBA if you’re lucky enough to have one. Hibernate is not about "transparent persistence" for Java objects. It’s about making two excellent technologies work smoothly together.

  2. Log the SQL executed by Hibernate. You cannot know that your persistence logic is correct until you’ve actually inspected the SQL that’s being executed. Even when everything seems to be "working", there might be a lurking N+1 selects monster.

  3. Be careful when modifying bidirectional associations. In principle, you should update both ends of the association. But Hibernate doesn’t strictly enforce that, since there are some situations where such a rule would be too heavy-handed. Whatever the case, it’s up to you to maintain consistency across your model.

  4. Never leak a persistence context across threads or concurrent transactions. Have a strategy or framework to guarantee this never happens.

  5. When running queries that return large result sets, take care to consider the size of the session cache. Consider using a stateless session.

  6. Think carefully about the semantics of the second-level cache, and how the caching policies impact transaction isolation.

  7. Avoid fancy bells and whistles you don’t need. Hibernate is incredibly feature-rich, and that’s a good thing, because it serves the needs of a huge number of users, many of whom have one or two very specialized needs. But nobody has all those specialized needs. In all probability, you have none of them. Write your domain model in the simplest way that’s reasonable, using the simplest mapping strategies that make sense.

  8. When something isn’t behaving as you expect, simplify. Isolate the problem. Find the absolute minimum test case which reproduces the behavior, before asking for help online. Most of the time, the mere act of isolating the problem will suggest an obvious solution.

  9. Avoid frameworks and libraries that "wrap" JPA. If there’s any one criticism of Hibernate and ORM that sometimes does ring true, it’s that it takes you too far from direct control over JDBC. An additional layer just takes you even further.

  10. Avoid copy/pasting code from random bloggers or stackoverflow reply guys. Many of the suggestions you’ll find online just aren’t the simplest solution, and many aren’t correct for Hibernate 6. Instead, understand what you’re doing; study the Javadoc of the APIs you’re using; read the JPA specification; follow the advice we give in this document; go direct to the Hibernate team on Zulip. (Sure, we can be a bit cantankerous at times, but we do always want you to be successful.)

  11. Always consider other options. You don’t have to use Hibernate for everything.