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

Conversation

scscgit
Copy link

@scscgit scscgit commented Aug 17, 2025

Hibernate uses a misleading concept name of "read-only" for entities whose modifications to child associations are persisted.

Imagine the following use-case: a new Hibernate developer will figure out and assume they can wrap a session using

session.setDefaultReadOnly( true );

to achieve the behavior equivalent to detached entities. Developers will continue maintaining this so-called "read-only" branch of code execution and eventually modify a child association, as they consider this Entity to be a simple DTO, to be read by other functions in the code. As a result, a severe bug (or an actual data loss in production) will be introduced.

Hibernate Docs have terrible SEO indexing, but an AI found for me that in version 3.5 there was an excellent documentation:

It had mentioned the following critical points:

In some ways, Hibernate treats read-only entities the same as entities that are not read-only:

  • Hibernate cascades operations to associations as defined in the entity mapping.
  • Hibernate updates the version if the entity has a collection with changes that dirties the entity;
  • A read-only entity can be deleted.

I wasn't able to find any similar information in the latest docs (7.1). All the sections arbitrarily link to each other in a way that there is no single place dedicated to describing the concept of read-only anymore.

I followed the reference of

| `Query#setReadOnly()` | Overrides the session-level default for read-only state. The concept of read-only state is covered in <<chapters/pc/PersistenceContext.adoc#pc,Persistence Contexts>>.

and added a warning to Persistence Contexts.

Similarly, there was an example of a usage in Read-only entities native query example, so I added an explicit cascade usage example below that.

  • A corresponding unit test was added, asserting the current behavior (mutating the test data before subsequently returning the test data to its previous state).

⚠️ I updated both the Session and Query (SelectionQuery)'s JavaDocs to explicitly document this behavior inside the setReadOnly and setDefaultReadOnly methods. This is the least we can do - I'd actually propose that you could consider renaming the methods.

  • Note that I only referenced my primary concern of a child cascade, to point out that it can't be trusted by default if users don't perform any analysis. However, I'm sure you'll figure out a more exact and complete definition. My suggestion is for you to quickly process this PR and then create a long-standing ticket for a more exhaustive review to be finished at a later point.

Please perform a thorough review of the documentation from the standpoint of ensuring that this concept is searchable.

Additionally, please consider preparing examples of correct usages for users who want to use an actual read-only state, especially at the beginning (right after opening a session). These patterns are currently very difficult to learn and understand. Namely, it's very dangerous to assume all developers will remember to evict an entity before mutating it if we don't open a read-only transaction. (We're currently analyzing the downsides of read-only transactions.)


I also found one ticket that may be related, which seems to consider the idea of "fixing" the behavior of read-only to be what one would intuitively assume (though it would be a breaking change):


  • 🗒️ Note: The Account entity was missing in the example data model, which I noticed when trying to reproduce your example from Docs, so I also fixed it - please review the comment-docs references etc.
  • 🗒️ Note: I added dashes inside Read-only entities native query example, because it's very misleading to include a phrase native query, which is a very distinct concept.
  • 🗒️ Note: I fixed a entity-immutability reference that linked to an anchor that no longer exists (clicking on the link had no effect).
    • 📓 Sub-note: this link to entity immutability should be reviewed, because it doesn't really relate to the concept of read-only. I mean, it kinda does, but it's very ambiguous, as it mentions that the mutability has various relative meanings; but it doesn't explicitly describe the read-only-kind of mutability. (It's far from the usefulness of the old docs.)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license
and can be relicensed under the terms of the LGPL v2.1 license in the future at the maintainers' discretion.
For more information on licensing, please check here.


….java - read-only parent's operations are still cascaded to its child associations
@hibernate-github-bot
Copy link

hibernate-github-bot bot commented Aug 17, 2025

Thanks for your pull request!

This pull request does not follow the contribution rules. Could you have a look?

❌ All commit messages should start with a JIRA issue key matching pattern HHH-\d+
    ↳ Offending commits: [67c8508]

› This message was automatically generated.

Copy link
Member

@gavinking gavinking left a comment

Choose a reason for hiding this comment

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

I'm not at all keen on these proposed changes, which seem to reflect a confusion about the whole notion of "read-only" and what it is about.

Read-only mode refers to the entity being in a state where its changes to its fields are not tracked by Hibernate and not made persistent. It is not relevant to what happens to the state of other associated entities.

For example, a child entity is the owner of the association to its parent, the FK is not a field of the parent entity, and so the readonliness of the parent is irrelevant.

This is, I believe, failry adequately explained in the Javadoc for Session.setReadOnly() and Session.setDefaultReadOnly().

Comment on lines +422 to +425
* 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>
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.

Comment on lines +390 to +393
* 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>
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...

@@ -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!

@@ -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]
====
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.

====
[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.

@scscgit
Copy link
Author

scscgit commented Aug 17, 2025

It is not relevant to what happens to the state of other associated entities.

My primary concern is that there was no change to any state of a child entity, because the child entity was transient.

This is, I believe, failry adequately explained in the Javadoc for Session.setReadOnly() and Session.setDefaultReadOnly().

To quote setDefaultReadOnly's only relevant parts:

  • Change the default for entities and proxies loaded into this session from modifiable to read-only mode
  • Read-only entities can be modified, but changes are not persisted.

There is not a single word about this peculiar case. If anything, it misleadingly pretends that we should actually be able to trust this mode. It says the "entity" can be safely modified, not "some of entity's fields that are directly owned".

@gavinking
Copy link
Member

gavinking commented Aug 17, 2025

Hibernate uses a misleading concept name of "read-only" for entities whose modifications to child associations are persisted.

Perhaps if you could explain where in the documentation of Hibernate you picked up on the idea that read-only mode for the parent had something to do with state mapped by the child, I could do a better job of reviewing this PR. As it is, I've no idea where you got this idea. It's not implied by the Javadoc. To me, it's pretty clear that foreign keys belong to child entities, not to parent entities.

a new Hibernate developer will figure out and assume they can wrap a session using setDefaultReadOnly( true ) to achieve the behavior equivalent to detached entities

I have quite literally no idea why a new developer would make this assumption. To two concepts are not related, and are not linked together in any documentation that I know of.

@gavinking
Copy link
Member

gavinking commented Aug 17, 2025

There is not a single word about this peculiar case. If anything, it misleadingly pretends that we should actually be able to trust this mode. It says the "entity" can be safely modified, not "some of entity's fields that are directly owned".

Ok, so, reading between the lines here, I think the mental model you're working from is that setReadOnly() is an operation that cascades from parent to child. But that's just not the case. None of the operations of Session cascade from parent to child by default, and cascading is not even defined for setReadOnly(). So its only effect is on the parent entity itself. The operation has no effect at all on children of the read-only entity.

[And the operation definitely has no effect on a new or detached child, because a new or detached entity can't possibly be in read-only mode.]

@scscgit
Copy link
Author

scscgit commented Aug 17, 2025

I have quite literally no idea why a new developer would make this assumption. To two concepts are not related, and are not linked together in any documentation that I know of.

Well, I can only tell you that I have quite literally no idea why any new developer would not make this assumption 😄 If you set it on a session, you should automatically inherit it on both parents and children. But yes, I wasn't literally implying equivalent to detached entities - there is obviously a difference in lazy loading and so on, but I meant the simple purpose of loading an entity and using it as a DTO without persisting changes on its own fields.

where in the documentation of Hibernate you picked up on the idea that read-only mode for the parent had something to do with state mapped by the child

None of the operations of Session cascade from parent to child by default

the operation definitely has no effect on a new or detached child, because a new or detached entity can't possibly be in read-only mode

Well my point is that I didn't pick it up in a documentation, because it was missing in the documentation. So I had to assume the intuitive behavior applies.

But again, I am not saying about cascading operations "to children" - especially not to persistent children. I am talking about cascading an operation to "persisting a new child, which was added as a transient entity to a read-only DTO". (You literally create an object via new and them the read-only session will automatically persist it.) There is a great difference... so I'm hoping someone will come to the defense of my PoV - I'd especially like to hear the opinions of both new and seasoned devs, as I believe this isn't intuitive to either of them :) Plus btw. my current goal is to find a safe equivalent to setDefaultReadOnly without having to use an actual read-only transaction.

@gavinking
Copy link
Member

gavinking commented Aug 17, 2025

My primary concern is that there was no change to any state of a child entity, because the child entity was transient.

But there actually was a change to the state of a child entity. You called child.setParent(parent) and placed the child entity in a collection with cascade=PERSIST. That's exactly the same thing as calling persist(child). And nothing anywhere in the Hibernate docs suggests that calling setReadOnly() on a parent turns off cascades. Because it doesn't, and, indeed, read-only mode was deliberately designed (not by me) to not turn off cascades. I know this because I found tests asserting it.

Now, all that said, I actually happen to agree that this behavior is less-than-intuitive. I even opened this issue a while ago:

https://hibernate.atlassian.net/issues/?filter=21198&selectedIssue=HHH-19398

If it weren't a breaking change, and if it weren't for those tests, I probably would turn off cascade processing for read-only entities.

And so, sure, I'm OK with stating explicitly that read-only mode has no relationship with and no effect on cascade processing. But I don't like anything else about how you're framing this discussion. The documentation accurately describes the effect of read-only mode.

@gavinking
Copy link
Member

Well, I can only tell you that I have quite literally no idea why any new developer would not make this assumption 😄 If you set it on a session, you should automatically inherit it on both parents and children. But yes, I wasn't literally implying equivalent to detached entities - there is obviously a difference in lazy loading and so on, but I meant the simple purpose of loading an entity and using it as a DTO without persisting changes on its own fields.

Alright, look, while I agree with you on the limited point that it might be more intuitive to turn of cascade persist for read-only entities, I really do think the rest of what you're saying here is a bit nuts. I don't think any of these expectations are reasonable, given the semantics of Session and its other operations. And I don't think there's anything in the existing documentation which would justify these expectations.

@scscgit
Copy link
Author

scscgit commented Aug 17, 2025

Oh, so the only issue which I found and linked in the original post is your issue, that's great 😃 Well yeah, I'm framing the discussion primarily with the intention of shining a light on the fact that it isn't intuitive. It's not just a tiny issue, but a problem where seasoned devs can be using Hibernate with this feature for 10 years and only today figure out a "bug" as they modify a child for the very first time... I'm strongly opinionated on this being a design flaw of the API, so I'm hoping for the best solution in the form of waiting for a major version to deprecate & rename this method. (And tbh, people often don't really memorize the entire docs, so it's common to actually forget these edge cases even if you learn them once. I usually rely on JavaDocs to double-check the definition as long as it's written in a user-friendly way.)

Thanks for the technical explanation of the implicit persist flow. Now it does make sense from a technical standpoint, but it's definitely crazy that this problem appeared in the first place :) We can never have enough English words for concepts; there are so many "read-onlys", so many "mutables", and we need both low-level and high-level ones... (Based on that definition, I subjectively re-classify it from a high-level & safe to a low-level "only use for very specific cases" API and intend on never using it again if I can avoid it.)

I don't think any of these expectations are reasonable

And I don't think there's anything in the existing documentation which would justify these expectations.

I mean, if you exclude my imperfect terminology (where I made the analogy between read-only session and detached entities), can you list which of my expectations do you consider unreasonable? Honestly, literally speaking, I do agree with you that "there's nothing in the existing documentation which would justify these expectations", because that's my primary complaint: there is nothing in the existing documentation 😅 it doesn't say it "doesn't persist new children", but how can a developer make the assumption that it means it "does persist them"? 😄 Would you honestly come to that conclusion if you were not a Hibernate contributor?

In any case, I believe we're clear on this issue, so thanks for all the details. I'd suggest inviting a few reviewers to suggest & review possible JavaDocs or any relevant places in the docs, so that some simple explanation of the current behavior can be merged asap.

@gavinking
Copy link
Member

gavinking commented Aug 17, 2025

Well yeah, I'm framing the discussion primarily with the intention of shining a light on the fact that it isn't intuitive.

Sure, I guess the disconnect is I see this more as a functional problem rather than a documentation problem. My preference would be to change the actual behavior, but we can't take any action on that until we see what happens with this stuff in JPA 4. (I've proposed to introduce a read-only mode to the spec.)

I'm strongly opinionated on this being a design flaw of the API

I wasn't involved in the design of this, so I'm really not clear on what were the considerations which went into the current behavior, but I do see that they were intentional decisions, because we have tests.

That said, I do think that:

  1. owned collections should be put in a read-only or immutable mode, and
  2. we should probably turn off cascade persist on read-only entities, though that still needs further consideration.

Actually there was no issue for (1) so I just opened https://hibernate.atlassian.net/browse/HHH-19715.

I subjectively re-classify it from a high-level & safe to a low-level "only use for very specific cases" API and intend on never using it again if I can avoid it.)

Absolutely correct. Read-only mode is essentially just a performance optimization for cases where you have a large number of entities in the session and want to manage the cost of dirty-checking. It's really a very low-level thing, which is why you won't see it mentioned in many places.

If this becomes part of JPA 4, we'll need to make sure the semantics are much more thoroughly reconsidered.

@scscgit
Copy link
Author

scscgit commented Aug 17, 2025

Yeah, it'll be great if you can change the behavior for collections, thanks for opening the ticket. I'm sure there's enough space for feature flags or new method overloads in the worst case if multiple solutions need to be supported; I mainly care about newcomers having the ability to open the docs and find one recommended default solution with an intuitive explanation of where it should be used. In my team's case, we just need to cover the independent needs of both "read only transaction" and "read only hibernate entities".

That said, it's still vital to merge my documentation changes, because many developers will keep using old versions for a very long time. Honestly, I'm hoping that it'll be possible for maintainers to backintegrate these doc changes into old Hibernate versions too, because it's certainly not a problem that appeared in the version 7.1. Considering old docs were documented correctly (though still not in JavaDocs I guess), it may need some individual consideration. I'm just hoping this issue can be considered serious enough to warrant any doc backintegration effort :)

One more thing: if we do indeed intend to use the setDefaultReadOnly API under the current versions, I was considering researching a workaround in the form of also adding a PersistEventListener to the session, to reject any attempts to persist a new entity, and maybe with FlushEntityEventListener. -> Actually, the combination of PreInsertEventListener, PreUpdateEventListener, and PreDeleteEventListener (for orphanRemoval) seems more fit. Despite some waste of performance, it would let us wrap the session in a safe API, though indirectly. As with everything, there may be risks due to having missed some edge cases, so I'd like to add this concept for a consideration when writing the docs regarding this primary use-case. If you could integrate a perfect workaround to be an officially supported solution by documenting it together with that API, it would also present a perfect transition process for anyone who intends to migrate to a fully-intuitive read-only session.

  • 🗒️ Or Interceptor could make even more sense than EventListener if we were okay with throwing an exception. To silently ignore persists, event listener seems to be necessary; however, a globally-registered listener must be conditionally activated by a ThreadLocal, so it's not exactly as clean as configuring one when opening a session. But what if we have two sessions on the same thread, etc.? Maybe a static map would help, vetoPersistSessions = Collections.synchronizedMap(new WeakHashMap<Session, Boolean>()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants