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