JEP 468 updating non-updatable fields

Brian Goetz brian.goetz at oracle.com
Mon Jan 26 15:54:47 UTC 2026


I think the entity-modeling story here is more like:

  - a regular class that associates (id, PersonInfo), where the ids are 
dispensed exclusively by the ORM, and
  - a record/carrier for PersonInfo, that lets you "mutate" the 
information but not the ID association.



On 1/26/2026 10:52 AM, Aaryn Tonita wrote:
> This past sprint we had such a case where unconditional deconstruction 
> would have helped with database entities. Basically a user had created 
> a patient twice over quite some time span and operations and graft 
> allocations were associated with both patients but the medical and 
> personal details were most accurate on the newest and the user desire 
> was to merge them. Our deduplication detection didn't trigger because 
> of incompleteness of the old record. However because so many 
> downstream systems depend on the oldest record that was the id to keep.
>
> In the database you would just alter the id of the old with cascade, 
> then relink the foreign key constraints of related tables to the new 
> patient, then modify the id back to the old value again with cascade 
> and delete the old. Now JPA doesn't support this approach of altering 
> a primary key... So instead you need to fetch the new and old entity 
> and deconstruct the new entity before deleting it and then reconstruct 
> the old entity with the new data and same old id... Or you reach for 
> the @Query approach instead and after modifying the database you 
> change just the id (and linked relations) of the newer patient 
> representation object. This latter approach is less brittle to future 
> changes.
>
> But the original point that Brian made stands: the constructor always 
> allows a nonsense representation. People exploit that in unit tests to 
> create unpersisted entities or relations to other entities that don't 
> exist. Without fetching the entire database all at once you won't 
> really get away from that but I also wouldn't want to.
>
> We have more and more places where withers would help (and sad places 
> where a carrier class would have helped but we used a class in place 
> of a record).
>
>
>
>
> Sent from Proton Mail 
> <https://urldefense.com/v3/__https://proton.me/mail/home__;!!ACWV5N9M2RV99hQ!M9c8nZS-3Ga3cPLqwU5QkNrUJs_RoN8etK5ZrHbzmmz29wzkUXoSJmTLdIVhe9GYuWhACJi2C0eIRWF10YI$> 
> for Android.
>
>
>
> -------- Original Message --------
> On Monday, 01/26/26 at 16:13 Ethan McCue <ethan at mccue.dev> wrote:
>
>     My immediate thought (aside from imagining Brian trapped in an
>     eternal version of that huffalumps and woozles scene from Winnie
>     the Pooh, but it's all these emails) is that database entities
>     aren't actually good candidates for "unconditional deconstruction"
>
>     I think this because the act of getting the data from the
>     db/persistence context is intrinsically fallible *and* attached to
>     instance behavior; maybe we need to look forward to what the
>     conditional deconstruction story would be?
>
>     On Mon, Jan 26, 2026, 10:04 AM Brian Goetz
>     <brian.goetz at oracle.com> wrote:
>
>
>
>>         It's interesting that when language designers make the code
>>         easier to write, somebody may complain that it's too easy :-)
>
>         I too had that "you can't win" feeling :)
>
>         I would recast the question here as "Can Java developers
>         handle carrier classes".  Records are restricted enough to
>         keep developers _mostly_ out of trouble, but the desire to
>         believe that this is a syntactic and not semantic feature is a
>         strong one, and given that many developers education about how
>         the language works is limited to "what does IntelliJ suggest
>         to me", may not even _realize_ they are giving into the dark
>         side.
>
>         I think it is worth working through the example here for "how
>         would we recommend handling the case of a "active" row like this.
>
>>         I think it's a perfect place for static analysis tooling. One
>>         may invent an annotation like `@NonUpdatable`
>>         with the `RECORD_COMPONENT` target and use it on such fields,
>>         then create an annotation processor
>>         (ErrorProne plugin, IntelliJ IDEA inspection, CodeQL rule,
>>         etc.), that will check the violations and fail the build if
>>         there are any.
>>         Adding such a special case to the language specification
>>         would be an overcomplication.
>>
>>         With best regards,
>>         Tagir Valeev.
>>
>>         On Sun, Jan 25, 2026 at 11:48 PM Brian Goetz
>>         <brian.goetz at oracle.com> wrote:
>>
>>             The important mental model here is that a reconstruction
>>             (`with`) expression is "just" a syntactic optimization for:
>>
>>              - destructure with the canonical deconstruction pattern
>>              - mutate the components
>>              - reconstruct with the primary constructor
>>
>>             So the root problem here is not the reconstruction
>>             expression; if you can bork up your application state
>>             with a reconstruction expression, you can bork it up
>>             without one.
>>
>>             Primary constructors can enforce invariants _on_ or
>>             _between_ components, such as:
>>
>>                 record Rational(int num, int denom) {
>>                     Rational { if (denom == 0) throw ... }
>>                 }
>>
>>             or
>>
>>                 record Range(int lo, int hi) {
>>                     Range { if (lo > hi) throw... }
>>                 }
>>
>>             What they can't do is express invariants between the
>>             record / carrier state and "the rest of the system",
>>             because they are supposed to be simple data carriers, not
>>             serialized references to some external system. A class
>>             that models a database row in this way is complecting
>>             entity state with an external entity id. By modeling in
>>             this way, you have explicitly declared that
>>
>>                 rec with { dbId++ }
>>
>>             *is explicitly OK* in your system; that the components of
>>             the record can be freely combined in any way (modulo
>>             enforced cross-component invariants).  And there are
>>             systems in which this is fine!  But you're imagining
>>             (correctly) that this modeling technique will be used in
>>             systems in which this is not fine.
>>
>>             The main challenge here is that developers will be so
>>             attracted to the syntactic concision that they will
>>             willfully ignore the semantic inconsistencies they are
>>             creating.
>>
>>
>>
>>
>>             On 1/25/2026 1:37 PM, Andy Gegg wrote:
>>>             Hello,
>>>             I apologise for coming late to the party here - Records
>>>             have been of limited use to me but Mr Goetz's email on
>>>             carrier classes is something that would be very useful
>>>             so I've been thinking about the consequences.
>>>
>>>             Since  carrier classes and records are for data, in a
>>>             database application somewhere or other you're going to
>>>             get database ids in records:
>>>             record MyRec(int dbId, String name,...)
>>>
>>>             While everything is immutable this is fine but JEP 468
>>>             opens up the possibility of mutation:
>>>
>>>             MyRec rec = readDatabase(...);
>>>             rec = rec with {name="...";};
>>>             writeDatabase(rec);
>>>
>>>             which is absolutely fine and what an application wants
>>>             to do.  But:
>>>             MyRec rec = readDatabase(...);
>>>             rec = rec with {dbId++;};
>>>             writeDatabase(rec);
>>>
>>>             is disastrous.  There's no way the canonical constructor
>>>             invoked from 'with' can detect stupidity nor can
>>>             whatever the database access layer does.
>>>
>>>             In the old days, the lack of a 'setter' would usually
>>>             prevent stupid code - the above could be achieved,
>>>             obviously, but the code is devious enough to make people
>>>             stop and think (one hopes).
>>>
>>>             Here there is nothing to say "do not update this!!!"
>>>             except code comments, JavaDoc and naming conventions.
>>>
>>>             It's not always obvious which fields may or may not be
>>>             changed in the application.
>>>
>>>             record MyRec(int dbId, int fatherId,...)
>>>             probably doesn't want
>>>             rec = rec with { fatherId = ... }
>>>
>>>             but a HR application will need to be able to do:
>>>
>>>             record MyRec(int dbId, int departmentId, ...);
>>>             ...
>>>             rec = rec with { departmentId = newDept; };
>>>
>>>             Clearly, people can always write stupid code (guilty...)
>>>             and the current state of play obviously allows the
>>>             possibility (rec = new MyRec(rec.dbId++, ...);) which is
>>>             enough to stop people using records here but carrier
>>>             classes will be very tempting and that brings derived
>>>             creation back to the fore.
>>>
>>>             It's not just database ids which might need restricting
>>>             from update, e.g. timestamps (which are better done in
>>>             the database layer) and no doubt different applications
>>>             will have their own business case restrictions.
>>>
>>>             Thank you for your time,
>>>             Andy Gegg
>>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20260126/fe3af0b9/attachment-0001.htm>


More information about the amber-dev mailing list