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 <[email protected]> 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 <[email protected]> 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 <[email protected]> >> <[email protected]> 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 <[email protected]> >> 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 <[email protected]> >>> 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 >>>> >>>> >>>> >>> >> >
