JEP 468 updating non-updatable fields

Ethan McCue ethan at mccue.dev
Mon Jan 26 16:18:44 UTC 2026


I'm alluding to state beyond the right to choose ids, but yes: (<id, misc.
bookkeeping>, PersonData)



On Mon, Jan 26, 2026, 11:15 AM Brian Goetz <brian.goetz at oracle.com> wrote:

> 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>
>> <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/910d691a/attachment-0001.htm>


More information about the amber-dev mailing list