JEP 468 updating non-updatable fields

Brian Goetz brian.goetz at oracle.com
Mon Jan 26 16:15:00 UTC 2026


I think we might be saying the same thing.  When I say "regular class", 
it doesn't matter whether it is hand written or generated; the key is 
that the right to choose IDs is reserved by that class.  Maybe its 
hidden behind an interface, maybe not.  But you have an _entity_ class 
that associates (id, PersonData) and a record/carrier for PersonData, 
which is _just_ the data.

On 1/26/2026 11:05 AM, Ethan McCue wrote:
> Forgive me if I'm mixing up terms here, but I think a database entity 
> when fetched from some persistence context can actually be a runtime 
> generated subclass that also maintains a reference to the context that 
> produced it.
>
> So you'd have an instance of Person and be tempted to write p = p with 
> { name = "..."; }. This could maybe work if the subclass could do its 
> own thing, but if you are feeding the data back into new Person(...) 
> it can't.
>
>
>
> On Mon, Jan 26, 2026, 10:55 AM Brian Goetz <brian.goetz at oracle.com> wrote:
>
>     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>
>>     <mailto: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/4e7d5119/attachment-0001.htm>


More information about the amber-dev mailing list