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]> 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