Skip to content

Docs: read-only parent's operations are still cascaded to its child associations #10772

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -1134,6 +1134,11 @@ Additionally, the `CascadeType.ALL` will propagate any Hibernate-specific operat
`REPLICATE`:: cascades the entity replicate operation.
`LOCK`:: cascades the entity lock operation.

[WARNING]
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
[WARNING]
[NOTE]

Copy link
Author

Choose a reason for hiding this comment

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

Just to confirm, you suggest changing it to a note here, but keeping it as a warning below? Also, maybe you could provide some reasoning for future maintainers why this is not semantically more of a warning of a non-intuitive behavior with potentially dangerous default usage. In any case, I agree that it should probably be just a note here, while the official definition with any warnings should be located in a document dedicated to "read-onliness", similar to Hibernate 3's docs. Thanks!

====
When a parent entity is in a **read-only state**, operations are still cascaded to its child associations. For example, adding a new child to a collection on a read-only parent will result in the child entity being persisted. If this behavior is not desired, the parent entity needs to be evicted from the session before making modifications.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
When a parent entity is in a **read-only state**, operations are still cascaded to its child associations. For example, adding a new child to a collection on a read-only parent will result in the child entity being persisted. If this behavior is not desired, the parent entity needs to be evicted from the session before making modifications.
When a parent entity is in a read-only mode, operations are still cascaded to its child associations. For example, adding a new child to a collection on a read-only parent will result in the child entity being persisted. If this behavior is not desired, the parent entity needs to be evicted from the session before making modifications.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, this section is not dealing with read-only mode at all, so this note is inappropriate in here. Please remove it.

Copy link
Author

Choose a reason for hiding this comment

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

As I mentioned in the ticket, the reason why I added it here is because it's literally the only place referenced by:
The concept of read-only state is covered in <<chapters/pc/PersistenceContext.adoc#pc,Persistence Contexts>>
When you follow this link, you won't find any other place in the documentation. But I'm not sure where exactly in the PersistenceContext is it correct to put it. Can you reconsider redirecting that link in the first place?

Side notes:

  • In those Query docs, there is one more mention of
    `org.hibernate.readOnly` | `true` if entities and collections loaded by this query should be marked as read-only.
    
    that doesn't link to any readOnly explanation. People who only find this non-"native" option in the docs need to also be aware of this.
  • You changed read-only state to read-only mode, but as you see in the fragment I quoted, the terminology of concept of read-only state is also used. Should it be changed there?

====

The following examples will explain some of the aforementioned cascade operations using the following entities:

[source, java, indent=0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ include::{example-dir-model}/PhoneType.java[tags=hql-examples-domain-model-examp

include::{example-dir-model}/Call.java[tags=hql-examples-domain-model-example]

include::{example-dir-model}/Account.java[tags=hql-examples-domain-model-example]

include::{example-dir-model}/Payment.java[tags=hql-examples-domain-model-example]

include::{example-dir-model}/CreditCardPayment.java[tags=hql-examples-domain-model-example]
Expand Down Expand Up @@ -452,7 +454,7 @@ See the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hi
[[hql-read-only-entities]]
==== Querying for read-only entities

As explained in <<chapters/domain/immutability.adoc#entity-immutability,entity immutability>>, fetching entities in read-only mode is more efficient than fetching entities whose state changes might need to be written to the database.
As explained in <<chapters/domain/immutability.adoc#mutability-entity,entity immutability>>, fetching entities in read-only mode is more efficient than fetching entities whose state changes might need to be written to the database.
Fortunately, even mutable entities may be fetched in read-only mode, with the benefit of reduced memory footprint and of a faster flushing process.

Read-only entities are skipped by the dirty checking mechanism as illustrated by the following example:
Expand All @@ -476,14 +478,28 @@ In this example, no SQL `UPDATE` was executed.
The method `Query#setReadOnly()` is an alternative to using a Jakarta Persistence query hint:

[[hql-read-only-entities-native-example]]
.Read-only entities native query example
.Read-only entities - native - query example
====
[source, java, indent=0]
----
include::{example-dir-query}/HQLTest.java[tags=hql-read-only-entities-native-example]
----
====

[WARNING]
====
When a parent entity is in a **read-only state**, operations are still cascaded to its child associations. For example, adding a new child to a collection on a read-only parent will result in the child entity being persisted. If this behavior is not desired, the parent entity needs to be evicted from the session before making modifications.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
When a parent entity is in a **read-only state**, operations are still cascaded to its child associations. For example, adding a new child to a collection on a read-only parent will result in the child entity being persisted. If this behavior is not desired, the parent entity needs to be evicted from the session before making modifications.
When a parent entity is in a read-only mode, operations are still cascaded to its child associations. For example, adding a new child to a collection on a read-only parent will result in the child entity being persisted. If this behavior is not desired, the parent entity needs to be evicted from the session before making modifications.

====

[[hql-read-only-entities-native-cascade-to-child-example]]
.Read-only entities - native, cascade to child - query example
====
[source, java, indent=0]
----
include::{example-dir-query}/HQLTest.java[tags=hql-read-only-entities-native-cascade-to-child-example]
----
====

[[hql-api-incremental]]
=== Scrolling and streaming results

Expand Down
4 changes: 4 additions & 0 deletions hibernate-core/src/main/java/org/hibernate/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,10 @@ public interface Session extends SharedSessionContract, EntityManager {
* Change the default for entities and proxies loaded into this session
* from modifiable to read-only mode, or from modifiable to read-only mode.
* <p>
* In some ways, Hibernate treats read-only entities the same as
* entities that are not read-only; for example, it cascades
* operations to associations as defined in the entity mapping.
* <p>
Comment on lines +422 to +425
Copy link
Member

Choose a reason for hiding this comment

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

I strongly disagree with this misleading language. Please remove it.

Cascading operations have absolutely nothing to do with read-onliness of an entity.

Copy link
Author

@scscgit scscgit Aug 17, 2025

Choose a reason for hiding this comment

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

I see what you mean: when the parent has a child association, which uses mappedBy to make it owned by the child, you don't view this child association as being one of the fields of the parent.

Personally, I strongly disagree with that perspective, because even though it is technically correct in some sense, I am not aware of any special definition of a "field" that excludes the @OneToMany from being considered a field. In a Hibernate entity, not all fields may be "persistable" for the purposes of a final query, but what I'm saying is that we are setting the read-only on the parent entity, implying all fields of this specific entity as defined by its data model, not only as defined by SQL.

Note that this is not the scenario where you explicitly persist the child. You only persist (via loading) the parent, which causes a side-effect of persistence even though you never "authorized" the persistence of the child itself.

I agree though that it should be re-phrased to include your definition that is more correct. My intention is to be explicit about covering this "severe risk" in the JavaDoc, and for that it is vital to explicitly inform the user of the practical side effect in the form of "persisting any added child association", which I also believe to be the most likely scenario to happen, as this is really close to the most trivial data model example.

For example, we could be more explicit about the definition by saying "This only changes all owned fields of an entity to be read-only. Any fields not owned by the entity, such as child associations, will not be considered read-only."

Can you help me come up with a reasonable comment here, considering this is the most frequent and important place any first-time users of this function will likely see? Thanks!

Copy link
Member

@gavinking gavinking Aug 17, 2025

Choose a reason for hiding this comment

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

Personally, I strongly disagree with that perspective, because even though it is technically correct in some sense, I am not aware of any special definition of a "field" that excludes the @onetomany from being considered a field. In a Hibernate entity, not all fields may be "persistable" for the purposes of a final query, but what I'm saying is that we are setting the read-only on the parent entity, implying all fields of this specific entity as defined by its data model, not only as defined by SQL.

Well, in this case at least, that's actually quite wrong. There is indeed a

special definition of a "field" that excludes the @OneToMany from being considered a field

This @OneToMany is an "unowned collection" in the language of JPA, and is not considered part of the state of the parent. If you make a change (only) to an unowned collection, that change is never made persistent.

Now, on the other hand, if this were an owned collection, you would have a strong point. I believe that the situation today is that read-only mode doesn't affect collections at all. And I think that's simply wrong. It should affect owned collections. But, again, this isn't really a problem with the documentation, it's a problem with the actual functionality.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, but what I'm saying is that the Read-Only's docs don't say fields except unowned fields or only owned fields (and especially not fields except unowned collections if the concept of unowned is not also explicitly used in the context of fields). It doesn't even use the word "fields", it merely says "Read-only entities can be modified, but changes are not persisted". It's quite a leap for any documentation reader to translate "entities" as "only fields that are not unowned collections".

  • Also, I'm not an expert in this terminology, but to me your definition seems as if my example was indeed an owned collection solely because of the fact that the change is made persistent, i.e. an insert is cascaded. I'm not saying your definition is technically wrong, but the statement itself is definitely not factual, because whatever the reason of persistence is, the persistence does happen. All I'm asking for is any doc fragment that explicitly references the fact that, in the end, "some persistence will happen".

In any case, as you explained the process very well, this JavaDoc should be very explicit about warning the user that cascading collections will still persist the entity.

Also, I will re-affirm my stance: if you read the Hibernate 3.5 readonly docs, you will find the sentence I added to this javadoc verbatim. It does in some ways "treat read-only entities the same as entities that are not read-only". It is literally about treating an entity as a whole this way. (And I can't imagine any JavaDoc reader would gain this understanding up until now.)

Thanks for reviewing the docs!

* Read-only entities are not dirty-checked and snapshots of persistent
* state are not maintained. Read-only entities can be modified, but
* changes are not persisted.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ default Stream<R> stream() {
* and proxies that are loaded into the session, use
* {@link Session#setDefaultReadOnly(boolean)}.
* <p>
* In some ways, Hibernate treats read-only entities the same as
* entities that are not read-only; for example, it cascades
* operations to associations as defined in the entity mapping.
* <p>
Comment on lines +390 to +393
Copy link
Member

Choose a reason for hiding this comment

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

Please remove.

Copy link
Author

Choose a reason for hiding this comment

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

Same as above; I'm hoping an expert in Hibernate terminology can fine-tune this critical point of visibility. This was the first place we analyzed when we observed this issue in a production code...

* Read-only entities are not dirty-checked and snapshots of persistent
* state are not maintained. Read-only entities can be modified, but
* changes are not persisted.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.persistence.FlushModeType;
Expand Down Expand Up @@ -3104,6 +3106,76 @@ public void test_hql_read_only_entities_native_example() {
});
}

@Test
public void test_hql_read_only_entities_native_cascade_to_child_example() {
AtomicReference<Long> cleanupAboveCallId = new AtomicReference<>();

doInJPA(
this::entityManagerFactory, entityManager -> {
//tag::hql-read-only-entities-native-cascade-to-child-example[]
Phone phone = (Phone) entityManager.createQuery(
"select p " +
"from Phone p " +
"where p.number = :phoneNumber ",
Phone.class
)
.setParameter( "phoneNumber", "123-456-7890" )
.unwrap( org.hibernate.query.Query.class )
.setReadOnly( true )
.list()
.get( 0 );
Call call = new Call();
call.setPhone( phone );
call.setDuration( 20 );
call.setTimestamp( LocalDateTime.of( 2000, 1, 1, 20, 0, 0 ) );
//end::hql-read-only-entities-native-cascade-to-child-example[]
cleanupAboveCallId.set(
phone.getCalls().stream().max( Comparator.comparing( Call::getId ) ).get().getId() );
//tag::hql-read-only-entities-native-cascade-to-child-example[]
phone.getCalls().add( call );
// The new Call child entity will be persisted during a transaction commit!
//end::hql-read-only-entities-native-cascade-to-child-example[]
assertEquals( 3, phone.getCalls().size() );
}
);

doInJPA(
this::entityManagerFactory, entityManager -> {
Phone phone = (Phone) entityManager.createQuery(
"select p " +
"from Phone p " +
"where p.number = :phoneNumber ",
Phone.class
)
.setParameter( "phoneNumber", "123-456-7890" )
.unwrap( org.hibernate.query.Query.class )
.setReadOnly( true )
.list()
.get( 0 );
assertEquals( 3, phone.getCalls().size() );
phone.getCalls().removeIf( call -> call.getId() > cleanupAboveCallId.get() );
assertEquals( 2, phone.getCalls().size() );
}
);

doInJPA(
this::entityManagerFactory, entityManager -> {
Phone phone = (Phone) entityManager.createQuery(
"select p " +
"from Phone p " +
"where p.number = :phoneNumber ",
Phone.class
)
.setParameter( "phoneNumber", "123-456-7890" )
.unwrap( org.hibernate.query.Query.class )
.setReadOnly( true )
.list()
.get( 0 );
assertEquals( 2, phone.getCalls().size() );
}
);
}

@Test
public void test_hql_derived_root_example() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;

//tag::hql-examples-domain-model-example[]
@Entity
public class Account {
@Id
Expand All @@ -25,6 +26,9 @@ public class Account {
@OneToMany(mappedBy = "account")
List<Payment> payments = new ArrayList<>();

//Getters and setters are omitted for brevity

//end::hql-examples-domain-model-example[]
public List<Payment> getPayments() {
return payments;
}
Expand All @@ -36,4 +40,7 @@ public Person getOwner() {
public void setOwner(Person owner) {
this.owner = owner;
}

//tag::hql-examples-domain-model-example[]
}
//end::hql-examples-domain-model-example[]