Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -187,102 +187,31 @@ If you want to load multiple entities by providing their identifiers, calling th
but also inefficient.

While the Jakarta Persistence standard does not support retrieving multiple entities at once, other than running a JPQL or Criteria API query,
Hibernate offers this functionality via the
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#byMultipleIds-java.lang.Class-[`byMultipleIds` method] of the Hibernate `Session`.

The `byMultipleIds` method returns a
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html[`MultiIdentifierLoadAccess`]
which you can use to customize the multi-load request.

The `MultiIdentifierLoadAccess` interface provides several methods which you can use to
change the behavior of the multi-load call:

`enableOrderedReturn(boolean enabled)`::
This setting controls whether the returned `List` is ordered and positional in relation to the
incoming ids. If enabled (the default), the return `List` is ordered and
positional relative to the incoming ids. In other words, a request to
`multiLoad([2,1,3])` will return `[Entity#2, Entity#1, Entity#3]`.
+
An important distinction is made here in regards to the handling of
unknown entities depending on this "ordered return" setting.
If enabled, a null is inserted into the `List` at the proper position(s).
If disabled, the nulls are not put into the return List.
+
In other words, consumers of the returned ordered List would need to be able to handle null elements.
`enableSessionCheck(boolean enabled)`::
This setting, which is disabled by default, tells Hibernate to check the first-level cache (a.k.a `Session` or Persistence Context) first and, if the entity is found and already managed by the Hibernate `Session`, the cached entity will be added to the returned `List`, therefore skipping it from being fetched via the multi-load query.
`enableReturnOfDeletedEntities(boolean enabled)`::
This setting instructs Hibernate if the multi-load operation is allowed to return entities that were deleted by the current Persistence Context. A deleted entity is one which has been passed to this
`Session.delete` or `Session.remove` method, but the `Session` was not flushed yet, meaning that the
associated row was not deleted in the database table.
+
The default behavior is to handle them as null in the return (see `enableOrderedReturn`).
When enabled, the result set will contain deleted entities.
When disabled (which is the default behavior), deleted entities are not included in the returning `List`.
`with(LockOptions lockOptions)`::
This setting allows you to pass a given
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/LockOptions.html[`LockOptions`] mode to the multi-load query.
`with(CacheMode cacheMode)`::
This setting allows you to pass a given
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html[`CacheMode`]
strategy so that we can load entities from the second-level cache, therefore skipping the cached entities from being fetched via the multi-load query.
`withBatchSize(int batchSize)`::
This setting allows you to specify a batch size for loading the entities (e.g. how many at a time).
+
The default is to use a batch sizing strategy defined by the `Dialect.getDefaultBatchLoadSizingStrategy()` method.
+
Any greater-than-one value here will override that default behavior.
`with(RootGraph<T> graph)`::
The `RootGraph` is a Hibernate extension to the Jakarta Persistence `EntityGraph` contract,
and this method allows you to pass a specific `RootGraph` to the multi-load query
so that it can fetch additional relationships of the current loading entity.
Hibernate offers this functionality via the `Session#findMultiple` methods which accepts a list of identifiers to load and a group of options which control certain behaviors of the loading -

* `ReadOnlyMode` - whether the entities loaded should be marked as read-only.
* `LockMode` (`LockModeType`) - a lock mode to be applied
* `Timeout` - if a pessimistic lock mode is used, a timeout to allow
* `Locking.Scope` (PessimisticLockScope`) - if a pessimistic lock mode is used, what scope should it be applied
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what scope should it be applied to ?

Copy link
Member Author

@sebersole sebersole Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire section needs a re-write. Feel free to tackle it :)

I'm just trying to improve it by removing out dated information - addition by subtraction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I was just indicating that perhaps the 'to' was missing, that's all

* `Locking.FollowOn` - allow (or not) Hibernate to acquire locks through additional queries if needed
* `CacheMode` (`CacheStoreMode` / `CacheRetrieveMode`) - how second level caching should be used, if at all
* `BatchSize` - how many identifiers should be loaded from the database at once
* `SessionChecking` - whether to look into the persistence context to check entity state
* `IncludeRemovals` - if `SessionChecking` is enabled, how removed entities should be handled
* `OrderedReturn` - whether the results should be ordered according to the order of the passed identifiers

Now, assuming we have 3 `Person` entities in the database, we can load all of them with a single call
as illustrated by the following example:

[[tag::pc-by-multiple-ids-example]]
.Loading multiple entities using the `byMultipleIds()` Hibernate API
.Loading multiple entities using the `findMultiple()` Hibernate API
====

[source, java, indent=0]
----
include::{example-dir-pc}/MultiLoadIdTest.java[tags=pc-by-multiple-ids-example]
include::{example-dir-pc}/FindMultipleDocTests.java[tags=pc-find-multiple-example]
----

[source, SQL, indent=0]
----
include::{extrasdir}/pc-by-multiple-ids-example.sql[]
----
====

Notice that only one SQL SELECT statement was executed since the second call uses the
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html#enableSessionCheck-boolean-[`enableSessionCheck`] method of the `MultiIdentifierLoadAccess`
to instruct Hibernate to skip entities that are already loaded in the current Persistence Context.

If the entities are not available in the current Persistence Context but they could be loaded from the second-level cache, you can use the
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html#with-org.hibernate.CacheMode-[`with(CacheMode)`] method of the `MultiIdentifierLoadAccess` object.

[[tag::pc-by-multiple-ids-second-level-cache-example]]
.Loading multiple entities from the second-level cache
====
[source, java, indent=0]
----
include::{example-dir-pc}/MultiLoadIdTest.java[tags=pc-by-multiple-ids-second-level-cache-example]
----
====

In the example above, we first make sure that we clear the second-level cache to demonstrate that
the multi-load query will put the returning entities into the second-level cache.

After executing the first `byMultipleIds` call, Hibernate is going to fetch the requested entities,
and as illustrated by the `getSecondLevelCachePutCount` method call, 3 entities were indeed added to the
shared cache.

Afterward, when executing the second `byMultipleIds` call for the same entities in a new Hibernate `Session`,
we set the
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html#NORMAL[`CacheMode.NORMAL`] second-level cache mode so that entities are going to be returned from the second-level cache.

The `getSecondLevelCacheHitCount` statistics method returns 3 this time, since the 3 entities were loaded from the second-level cache, and, as illustrated by `sqlStatementInterceptor.getSqlQueries()`, no multi-load SELECT statement was executed this time.

[[pc-find-natural-id]]
=== Obtain an entity by natural-id
Expand Down
29 changes: 20 additions & 9 deletions hibernate-core/src/main/java/org/hibernate/IncludeRemovals.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,32 @@
import java.util.List;

/**
* MultiFindOption implementation to specify whether the returned list
* of entity instances should contain instances that have been
* {@linkplain Session#remove(Object) marked for removal} in the
* current session, but not yet deleted in the database.
* When {@linkplain SessionChecking} is enabled, this option controls how
* to handle entities which are already contained by the persistence context
* but which are in a removed state (marked for removal, but not yet flushed).
* <p>
* The default is {@link #EXCLUDE}, meaning that instances marked for
* removal are replaced by null in the returned list of entities when {@link OrderedReturn}
* is used.
* The default is {@link #EXCLUDE}.
*
* @see org.hibernate.MultiFindOption
* @see OrderedReturn
* @see org.hibernate.Session#findMultiple(Class, List, FindOption...)
* @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...)
*
* @since 7.2
*/
public enum IncludeRemovals implements MultiFindOption {
/**
* Removed entities are included in the load result.
*/
INCLUDE,
/**
* The default. Removed entities are excluded from the load result.
* <p/>
* When combined with {@linkplain OrderedReturn#UNORDERED}, the entity is
* simply excluded from the result.
* <p/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I keep tripping over this.
If by "excluded from the load result" you mean that no nulls are present in the load result for removed instances when sessionchecking == enabled, includeremovals == exclude and ordering == unordered, then I can't get this one to work as expected:
e.g. running MultiLoadTest.testUnorderedMultiLoadFrom2ndLevelCachePendingDeleteReturnRemoved, with these settings returns a List of size 3, where one element is null, which seems to contradict what follows the IF above.
If what follows the IF above is NOT what you meant then my assumption is wrong.

Copy link
Member Author

@sebersole sebersole Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think EXCLUDE + UNORDERED is really the only thing confusing you?

My memory is just wrong for that case. It does appear that in this case the null for the removed entity is still included in the result. That's not what I remember and I have no reasonable explanation why that is the case. I mean, I can see maybe wanting to avoid needing to resize the list later or something like that, but from a user perspective I think that seems wrong.

Given that, I almost think the naming is wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think EXCLUDE + UNORDERED is really the only thing confusing you?

this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My memory is just wrong for that case. It does appear that in this case the null for the removed entity is still included in the result. That's not what I remember and I have no reasonable explanation why that is the case. I mean, I can see maybe wanting to avoid needing to resize the list later or something like that, but from a user perspective I think that seems wrong.

Given that, I almost think the naming is wrong.

So, change name, reword doc some more, or create a jira if you consider it's a bug?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not that I consider it a bug per-se. I just consider it less than obvious behavior. But I assume its probably always been this way. Perhaps a third option makes sense, though this third option would depend on the value for OrderedReturn.

enum IncludeRemovals {
    /** Legacy false option */
    INCLUDE,
    /** Legacy true option - replace with null */
    REPLACE,
    /** New option. */
    EXCLUDE
}

This new option would trigger the more (imo) appropriate behavior -

  • With ordered results, we replace with nulls. This is ofc needed to maintain ordering.
  • With unordered results, we'd not have any entry in the result for removed entities.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have a backward compatibility problem with that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With adding a new option? Why would that be a problem?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misinterpreted the reformulated options, my bad

* When combined with {@linkplain OrderedReturn#ORDERED}, the entity is replaced
* by {@code null} in the result.
*
* @see OrderedReturn
*/
EXCLUDE
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@
package org.hibernate;


import jakarta.persistence.EntityGraph;
import jakarta.persistence.FindOption;

import java.util.List;

/**
* Simple marker interface for FindOptions which can be applied to multiple id loading.
*
* @see org.hibernate.Session#findMultiple(Class, List, FindOption...)
* @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...)
*
* @since 7.2
*/
public interface MultiFindOption extends FindOption {
}
41 changes: 31 additions & 10 deletions hibernate-core/src/main/java/org/hibernate/OrderedReturn.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,43 @@
import java.util.List;

/**
* MultiFindOption implementation to specify whether the returned list
* of entity instances should be ordered, where the position of an entity
* instance is determined by the position of its identifier
* in the list of ids passed to {@code findMultiple(...)}.
* Indicates whether the result list should be ordered relative to the
* position of the identifier list. E.g.
* <pre>
* List&lt;Person&gt; results = session.findMultiple(
* Person.class,
* List.of(1,2,3,2),
* ORDERED
* );
* assert results.get(0).getId() == 1;
* assert results.get(1).getId() == 2;
* assert results.get(2).getId() == 3;
* assert results.get(3).getId() == 2;
* </pre>
* <p>
* The default is {@link #ORDERED}, meaning the positions of the entities
* in the returned list correspond to the positions of their ids. In this case,
* the {@link IncludeRemovals} handling of entities marked for removal
* becomes important.
* The default is {@link #ORDERED}.
*
* @see org.hibernate.MultiFindOption
* @see IncludeRemovals
* @see org.hibernate.Session#findMultiple(Class, List, FindOption...)
* @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...)
*
* @since 7.2
*/
public enum OrderedReturn implements MultiFindOption {
/**
* The default. The result list is ordered relative to the
* position of the identifiers list. This may result in {@code null}
* elements in the list - <ul>
* <li>non-existent identifiers
* <li>removed entities (when combined with {@linkplain IncludeRemovals#EXCLUDE})
* </ul>
* <p/>
* The result list will also always have the same length as the identifier list.
*
* @see IncludeRemovals
*/
ORDERED,
/**
* The result list may be in any order.
*/
UNORDERED
}
23 changes: 9 additions & 14 deletions hibernate-core/src/main/java/org/hibernate/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -577,9 +577,6 @@ public interface Session extends SharedSessionContract, EntityManager {
* <li>on the other hand, for databases with no SQL array type, a large batch size results
* in long SQL statements with many JDBC parameters.
* </ul>
* <p>
* For more advanced cases, use {@link #byMultipleIds(Class)}, which returns an instance of
* {@link MultiIdentifierLoadAccess}.
*
* @param entityType the entity type
* @param ids the list of identifiers
Expand All @@ -588,7 +585,9 @@ public interface Session extends SharedSessionContract, EntityManager {
* @return an ordered list of persistent instances, with null elements representing missing
* entities, whose positions in the list match the positions of their ids in the
* given list of identifiers
* @see #byMultipleIds(Class)
*
* @see MultiFindOption
*
* @since 7.0
*/
<E> List<E> findMultiple(Class<E> entityType, List<?> ids, FindOption... options);
Expand Down Expand Up @@ -617,9 +616,6 @@ public interface Session extends SharedSessionContract, EntityManager {
* <li>on the other hand, for databases with no SQL array type, a large batch size results
* in long SQL statements with many JDBC parameters.
* </ul>
* <p>
* For more advanced cases, use {@link #byMultipleIds(Class)}, which returns an instance of
* {@link MultiIdentifierLoadAccess}.
*
* @param entityGraph the entity graph interpreted as a load graph
* @param ids the list of identifiers
Expand All @@ -628,7 +624,9 @@ public interface Session extends SharedSessionContract, EntityManager {
* @return an ordered list of persistent instances, with null elements representing missing
* entities, whose positions in the list match the positions of their ids in the
* given list of identifiers
* @see #byMultipleIds(Class)
*
* @see MultiFindOption
*
* @since 7.0
*/
<E> List<E> findMultiple(EntityGraph<E> entityGraph, List<?> ids, FindOption... options);
Expand Down Expand Up @@ -1166,9 +1164,7 @@ public interface Session extends SharedSessionContract, EntityManager {
*
* @see #findMultiple(Class, List, FindOption...)
*
* @deprecated This method will be removed.
* Use {@link #findMultiple(Class, List, FindOption...)} instead.
* See {@link MultiFindOption}.
* @deprecated Use {@link #findMultiple(Class, List, FindOption...)} instead.
*/
@Deprecated(since = "7.2", forRemoval = true)
<T> MultiIdentifierLoadAccess<T> byMultipleIds(Class<T> entityClass);
Expand All @@ -1183,9 +1179,8 @@ public interface Session extends SharedSessionContract, EntityManager {
*
* @throws HibernateException If the given name does not resolve to a mapped entity
*
* @deprecated This method will be removed.
* Use {@link #findMultiple(Class, List, FindOption...)} instead.
* See {@link MultiFindOption}.
* @deprecated Use {@link #findMultiple(EntityGraph, List, FindOption...)} instead,
* with {@linkplain SessionFactory#createGraphForDynamicEntity(String)}.
*/
@Deprecated(since = "7.2", forRemoval = true)
<T> MultiIdentifierLoadAccess<T> byMultipleIds(String entityName);
Expand Down
30 changes: 21 additions & 9 deletions hibernate-core/src/main/java/org/hibernate/SessionChecking.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,32 @@
import java.util.List;

/**
* MultiFindOption implementation to specify whether the ids of managed entity instances already
* cached in the current persistence context should be excluded.
* from the list of ids sent to the database.
* <p>
* The default is {@link #DISABLED}, meaning all ids are included and sent to the database.
* Indicates whether the persistence context should be checked for entities
* matching the identifiers to be loaded - <ul>
* <li>Entities which are in a managed state are not re-loaded from the database.
* <li>Entities which are in a removed state are {@linkplain IncludeRemovals#EXCLUDE excluded}
* from the result by default, but can be {@linkplain IncludeRemovals#INCLUDE included} if desired.
* </ul>
* <p/>
* The default is {@link #DISABLED}
*
* Use {@link #ENABLED} to exclude already managed entity instance ids from
* the list of ids sent to the database.
*
* @see org.hibernate.MultiFindOption
* @see org.hibernate.Session#findMultiple(Class, List , FindOption...)
* @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...)
*
* @since 7.2
*/
public enum SessionChecking implements MultiFindOption {
/**
* The persistence context will be checked. Identifiers for entities already contained
* in the persistence context will not be sent to the database for loading. If the
* entity is marked for removal in the persistence context, whether it is returned
* is controlled by {@linkplain IncludeRemovals}.
*
* @see IncludeRemovals
*/
ENABLED,
/**
* The default. All identifiers to be loaded will be read from the database and returned.
*/
DISABLED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.pc;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.hibernate.LockMode.PESSIMISTIC_WRITE;
import static org.hibernate.OrderedReturn.ORDERED;

/**
* @author Steve Ebersole
*/
@SuppressWarnings("JUnitMalformedDeclaration")
@DomainModel(annotatedClasses = FindMultipleDocTests.Person.class)
@SessionFactory
public class FindMultipleDocTests {
@Test
void testUsage(SessionFactoryScope factoryScope) {
factoryScope.inTransaction( (session) -> {
//tag::pc-find-multiple-example[]
List<Person> persons = session.findMultiple(
Person.class,
List.of(1,2,3),
PESSIMISTIC_WRITE,
ORDERED
);
Comment on lines +53 to +58

Check notice

Code scanning / CodeQL

Unread local variable Note test

Variable 'List persons' is never read.
//end::pc-find-multiple-example[]
} );
}

@Entity(name="Person")
@Table(name="persons")
public static class Person {
@Id
private Integer id;
private String name;
}
}
Loading
Loading