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