Skip to content
82 changes: 63 additions & 19 deletions api/src/main/java/jakarta/persistence/EntityManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,9 @@ public non-sealed interface EntityManager extends EntityHandler {
* marked {@link CascadeType#PERSIST cascade=PERSIST}. If the given
* entity instance is already managed, that is, if it already belongs
* to this persistence context and has not been marked for removal,
* it is itself ignored, but the operation still cascades.
* it is itself ignored, but the operation still cascades. If the
* given entity is currently loaded in read-only mode, reset its mode
* to {@link ManagedEntityMode#READ_WRITE}.
* @param entity a new, managed, or removed entity instance
* @throws EntityExistsException if the given entity is detached
* (if the entity is detached, the {@code EntityExistsException}
Expand Down Expand Up @@ -225,7 +227,9 @@ public non-sealed interface EntityManager extends EntityHandler {
* entity instance is managed, that is, if it belongs to this
* persistence context and has not been marked for removal, it is
* itself ignored, but the operation still cascades, and it is
* returned directly.
* returned directly. If the returned entity is currently loaded
* in read-only mode, reset its mode to
* {@link ManagedEntityMode#READ_WRITE}.
* @param entity a new, managed, or detached entity instance
* @return the managed instance that the state was merged to
* @throws IllegalArgumentException if the instance is not an entity
Expand All @@ -245,11 +249,12 @@ public non-sealed interface EntityManager extends EntityHandler {
/**
* Mark a managed entity instance as removed, resulting in its deletion
* from the database when the persistence context is synchronized with
* the database. This operation cascades to every entity related by an
* association marked {@link CascadeType#REMOVE cascade=REMOVE}. If the
* given entity instance is already removed, it is ignored. If the
* given entity is new, it is itself ignored, but the operation still
* cascades.
* the database. If the given entity is currently loaded in read-only
* mode, reset its mode to {@link ManagedEntityMode#READ_WRITE}. This
* operation cascades to every entity related by an association marked
* {@link CascadeType#REMOVE cascade=REMOVE}. If the given entity
* instance is already removed, it is ignored. If the given entity is
* new, it is itself ignored, but the operation still cascades.
* @param entity a managed, new, or removed entity instance
* @throws IllegalArgumentException if the instance is not an entity
* or is a detached entity
Expand All @@ -260,6 +265,8 @@ public non-sealed interface EntityManager extends EntityHandler {
* @throws OptimisticLockException if an optimistic locking conflict
* is detected (note that optimistic version checking might be
* deferred until changes are flushed to the database)
* @throws PersistenceException if the given entity is currently loaded
* in read-only mode
*/
void remove(Object entity);

Expand Down Expand Up @@ -427,6 +434,8 @@ <T> T find(Class<T> entityClass, Object primaryKey,
/**
* Lock an entity instance belonging to the persistence context,
* obtaining the specified {@linkplain LockModeType lock mode}.
* If the given entity is currently loaded in read-only mode,
* reset its mode to {@link ManagedEntityMode#READ_WRITE}.
* <p>If a pessimistic lock mode type is specified and the entity
* contains a version attribute, the persistence provider must
* also perform optimistic version checks when obtaining the
Expand Down Expand Up @@ -465,7 +474,9 @@ <T> T find(Class<T> entityClass, Object primaryKey,
/**
* Lock an entity instance belonging to the persistence context,
* obtaining the specified {@linkplain LockModeType lock mode},
* using the specified properties.
* using the specified properties. If the given entity is currently
* loaded in read-only mode, reset its mode to
* {@link ManagedEntityMode#READ_WRITE}.
* <p>If a pessimistic lock mode type is specified and the entity
* contains a version attribute, the persistence provider must
* also perform optimistic version checks when obtaining the
Expand Down Expand Up @@ -513,7 +524,9 @@ void lock(Object entity, LockModeType lockMode,
/**
* Lock an entity instance belonging to the persistence context,
* obtaining the specified {@linkplain LockModeType lock mode},
* using the specified {@linkplain LockOption options}.
* using the specified {@linkplain LockOption options}. If the
* given entity is currently loaded in read-only mode, reset its
* mode to {@link ManagedEntityMode#READ_WRITE}.
* <p>If a pessimistic lock mode type is specified and the entity
* contains a version attribute, the persistence provider must
* also perform optimistic version checks when obtaining the
Expand Down Expand Up @@ -561,7 +574,9 @@ void lock(Object entity, LockModeType lockMode,
/**
* Refresh the state of the given managed entity instance from
* the database, overwriting unflushed changes made to the entity,
* if any. This operation cascades to every entity related by an
* if any. If the given entity is currently loaded in read-only
* mode, reset its mode to {@link ManagedEntityMode#READ_WRITE}.
* This operation cascades to every entity related by an
* association marked {@link CascadeType#REFRESH cascade=REFRESH}.
* @param entity a managed entity instance
* @throws IllegalArgumentException if the instance is not
Expand All @@ -580,7 +595,9 @@ void lock(Object entity, LockModeType lockMode,
/**
* Refresh the state of the given managed entity instance from
* the database, using the specified properties, and overwriting
* unflushed changes made to the entity, if any. This operation
* unflushed changes made to the entity, if any. If the given
* entity is currently loaded in read-only mode, reset its mode
* to {@link ManagedEntityMode#READ_WRITE}. This operation
* cascades to every entity related by an association marked
* {@link CascadeType#REFRESH cascade=REFRESH}.
* <p>If a vendor-specific property or hint is not recognized,
Expand All @@ -607,8 +624,10 @@ void refresh(Object entity,
* Refresh the state of the given managed entity instance from
* the database, overwriting unflushed changes made to the entity,
* if any, and obtain the given {@linkplain LockModeType lock mode}.
* This operation cascades to every entity related by an association
* marked {@link CascadeType#REFRESH cascade=REFRESH}.
* If the given entity is currently loaded in read-only mode, reset
* its mode to {@link ManagedEntityMode#READ_WRITE}. This operation
* cascades to every entity related by an association marked
* {@link CascadeType#REFRESH cascade=REFRESH}.
* <p>If the lock mode type is pessimistic, and the entity instance
* is found but cannot be locked:
* <ul>
Expand Down Expand Up @@ -648,9 +667,11 @@ void refresh(Object entity,
* Refresh the state of the given managed entity instance from
* the database, overwriting unflushed changes made to the entity,
* if any, and obtain the given {@linkplain LockModeType lock mode},
* using the specified properties. This operation cascades to every
* entity related by an association marked {@link CascadeType#REFRESH
* cascade=REFRESH}.
* using the specified properties. If the given entity is currently
* loaded in read-only mode, reset its mode to
* {@link ManagedEntityMode#READ_WRITE}. This operation cascades
* to every entity related by an association marked
* {@link CascadeType#REFRESH cascade=REFRESH}.
* <p>If the lock mode type is pessimistic, and the entity instance
* is found but cannot be locked:
* <ul>
Expand Down Expand Up @@ -700,9 +721,11 @@ void refresh(Object entity, LockModeType lockMode,
* database, using the specified {@linkplain RefreshOption options},
* overwriting changes made to the entity, if any. If the supplied
* options include a {@link LockModeType}, lock the given entity,
* obtaining the given lock mode. This operation cascades to every
* entity related by an association marked {@link CascadeType#REFRESH
* cascade=REFRESH}.
* obtaining the given lock mode. If the given entity is currently
* loaded in read-only mode, reset its mode to
* {@link ManagedEntityMode#READ_WRITE}. This operation cascades
* to every entity related by an association marked
* {@link CascadeType#REFRESH cascade=REFRESH}.
* <p>If the lock mode type is pessimistic, and the entity instance is
* found but cannot be locked:
* <ul>
Expand Down Expand Up @@ -752,6 +775,27 @@ void refresh(Object entity,
*/
void clear();

/**
* Obtain the {@link ManagedEntityMode} of the given entity.
* @param entity a persistent entity associated with the
* persistence context
* @throws IllegalArgumentException if the instance is not an entity
* or if the entity is not managed
* @since 4.0
*/
ManagedEntityMode getManagedEntityMode(Object entity);

/**
* Set the {@link ManagedEntityMode} of the given entity.
* @param entity a persistent entity associated with the
* persistence context
* @param mode the new mode
* @throws IllegalArgumentException if the instance is not an entity
* or if the entity is not managed
* @since 4.0
*/
void setManagedEntityMode(Object entity, ManagedEntityMode mode);

/**
* Evict the given managed or removed entity from the persistence
* context, causing the entity to become immediately detached.
Expand Down
108 changes: 108 additions & 0 deletions api/src/main/java/jakarta/persistence/ManagedEntityMode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/

// Contributors:
// Gavin King - 4.0

package jakarta.persistence;

/**
* A {@link FindOption} which specifies whether entities are
* to be loaded in {@linkplain #READ_ONLY read only} mode or
* in regular {@linkplain #READ_WRITE modifiable} mode.
* <p>
* By default, an entity is loaded in modifiable mode, and
* its state is synchronized to the database when a flush
* operation occurs.
* <p>
* An entity may be loaded in read-only mode by explicitly
* passing {@link #READ_ONLY} as an option to any method of
* {@link EntityManager} which accepts a {@link FindOption}
* or to {@link Query#setManagedEntityMode}. Furthermore,
* the current mode for a given managed entity may be changed
* by calling {@link EntityManager#setManagedEntityMode}.
* <p>
* Changes made to an entity instance currently loaded in
* read-only mode are not synchronized to the database and
* do not become persistent. The provider is not required
* to detect modifications to entities in read-only mode.
* <p>
* When {@link #READ_ONLY} is passed as an option, or when
* a query is executed in {@link #READ_ONLY} mode, as
* determined by the return value of the method
* {@link Query#setManagedEntityMode}, every entity loaded
* into the persistence context during the invocation is
* loaded in read-only mode, including any eagerly fetched
* associated entities. Entities which are already loaded
* remain in their predetermined {@link ManagedEntityMode}.
* <p>
* On the other hand, when the {@code ManagedEntityMode} of
* an entity is changed via invocation of
* {@link EntityManager#setManagedEntityMode}, the new mode
* applies only the given managed entity instance, and does
* not affect associated entities.
* <p>
* A managed entity in read-only mode has its mode
* automatically reset to {@link #READ_WRITE} when:
* <ul>
* <li>it is directly returned by a call to a method of
* {@link EntityManager}, and {@link #READ_ONLY} was
* not specified as an argument to the method,
* <li>it is referenced by the entity returned by a call
* to a method of {@link EntityManager} via an
* association path marked for eager fetching within
* the fetch graph in effect during execution of the
* method, and {@link #READ_ONLY} was not specified as
* an argument to the method,
* <li>it occurs directly in the result of a query which
* was executed in {@link #READ_WRITE} mode, as
* determined by the return value of the method
* {@link Query#getManagedEntityMode},
* <li>it is referenced via an association path marked
* for eager fetching by an entity occurring directly
* in the result of a query which was executed in
* {@link #READ_WRITE} mode, or
* <li>it is passed as an argument to
* {@link EntityManager#refresh refresh()},
* {@link EntityManager#lock lock()},
* {@link EntityManager#remove persist()}, or
* {@link EntityManager#remove remove()}.
* </ul>
* <p>If an entity in read-only mode is modified, and then the
* entity is reset to modifiable mode, either automatically or
* explicitly, the behavior is undefined. Such modifications
* might be lost; they might be made persistent; or the provider
* might throw an exception. Portable applications should not
* depend on such behavior.
*
* @since 4.0
*/
public enum ManagedEntityMode implements FindOption {
/**
* Specifies that an entity is loaded with the intention that
* its state is used only for reading, and not for modification,
* enabling the persistence provider to optimize performance.
* <p>
* If a read-only entity is modified, the modifications are not
* synchronized with the database. The provider is not required
* to detect modifications to entities in read-only mode.
*/
READ_ONLY,
/**
* Specifies that an entity should be loaded in the default
* modifiable mode.
* <p>
* Changes made to modifiable entities are synchronized with
* the database when the persistence context is flushed.
*/
READ_WRITE
}
8 changes: 8 additions & 0 deletions api/src/main/java/jakarta/persistence/NamedNativeQuery.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

// Contributors:
// Gavin King - 4.0
// Gavin King - 3.2
// Petros Splinakis - 2.2
// Linda DeMichiel - 2.1
Expand Down Expand Up @@ -95,6 +96,13 @@
*/
String query();

/**
* (Optional) The {@link ManagedEntityMode} to use for entities
* loaded during execution of the query.
* @since 4.0
*/
ManagedEntityMode managedEntityMode() default ManagedEntityMode.READ_WRITE;

/**
* The class of each query result. If a {@link #resultSetMapping
* result set mapping} is specified, the specified result class
Expand Down
7 changes: 7 additions & 0 deletions api/src/main/java/jakarta/persistence/NamedQuery.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@
*/
LockModeType lockMode() default LockModeType.NONE;

/**
* (Optional) The {@link ManagedEntityMode} to use for entities
* loaded during execution of the query.
* @since 4.0
*/
ManagedEntityMode managedEntityMode() default ManagedEntityMode.READ_WRITE;

/**
* (Optional) Query properties and hints. May include
* vendor-specific query hints.
Expand Down
19 changes: 18 additions & 1 deletion api/src/main/java/jakarta/persistence/Query.java
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ default Stream getResultStream() {
* @throws PersistenceException if the flush fails
* @throws OptimisticLockException if an optimistic locking
* conflict is detected during the flush
*
* @since 3.2
*/
Object getSingleResultOrNull();
Expand Down Expand Up @@ -627,6 +626,24 @@ Query setParameter(int position, Date value,
*/
Integer getTimeout();

/**
* Set the {@link ManagedEntityMode} to be used for entities
* loaded during execution of this query. Every instance of
* {@code Query} is created with the default mode
* {@link ManagedEntityMode#READ_ONLY}.
*
* @since 4.0
*/
Query setManagedEntityMode(ManagedEntityMode managedEntityMode);

/**
* The {@link ManagedEntityMode} that will be in effect during
* execution of this query.
*
* @since 4.0
*/
ManagedEntityMode getManagedEntityMode();

/**
* Return an object of the specified type to allow access to
* a provider-specific API. If the provider implementation of
Expand Down
11 changes: 11 additions & 0 deletions api/src/main/java/jakarta/persistence/TypedQuery.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public interface TypedQuery<X> extends Query {
* @throws PersistenceException if the query execution exceeds
* the query timeout value set and the transaction
* is rolled back
* @throws PersistenceException if the flush fails
* @throws OptimisticLockException if an optimistic locking
* conflict is detected during the flush
*/
List<X> getResultList();

Expand Down Expand Up @@ -397,4 +400,12 @@ TypedQuery<X> setParameter(int position, Date value,
* @since 3.2
*/
TypedQuery<X> setTimeout(Integer timeout);

/**
* Set the {@link ManagedEntityMode} to be used for entities
* loaded during execution of this query.
*
* @since 4.0
*/
TypedQuery<X> setManagedEntityMode(ManagedEntityMode managedEntityMode);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@

package jakarta.persistence.query;

import jakarta.persistence.CacheRetrieveMode;
import jakarta.persistence.CacheStoreMode;
import jakarta.persistence.LockModeType;
import jakarta.persistence.QueryHint;
import jakarta.persistence.*;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
Expand Down Expand Up @@ -68,6 +65,12 @@
*/
CacheRetrieveMode cacheRetrieveMode() default CacheRetrieveMode.USE;

/**
* The {@link ManagedEntityMode} to use for entities loaded
* during execution of the query.
*/
ManagedEntityMode managedEntityMode() default ManagedEntityMode.READ_WRITE;

/**
* A query timeout in milliseconds.
* By default, there is no timeout.
Expand Down
Loading
Loading