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]>
    <mailto:[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




Reply via email to