ORM Structural Mapping
Purpose
Get the structural mapping decisions right at the point where they are cheap, because each
one becomes a data migration afterwards. These patterns are where the impedance mismatch
actually lives: an object has references, a table has foreign keys; an object may contain a
value, a table has columns; an object graph has ownership, a schema has constraints.
The failure this prevents is mapping by autocompletion — @ManyToMany because there are two
collections, @GeneratedValue because it was in the tutorial, a JSON column because the
shape was awkward — and discovering the consequences at production volume.
The patterns
Identity Field the object carries the row's primary key, so the
mapper can find the row again. Choice of key type
and generation is a schema-level commitment.
Foreign Key Mapping an object reference becomes a foreign key column.
The owning side writes it; the other side is a view.
Association Table a many-to-many becomes a third table. The moment
Mapping that table needs an attribute, it is an entity.
Dependent Mapping a child's write lifecycle belongs to its parent;
an entity child still has an identifier.
Embedded Value a value object typically becomes owner-table columns.
No independent persistent identity or lifecycle.
Serialized LOB a graph is stored as one JSON/XML/binary column.
Opaque binary and native queryable JSON have different costs.
Workflow
- Choose identity with lifecycle and storage topology, because generation constrains batching,
sharding and when equality can be stable. It need not precede every domain decision.
- For each association, name the owner — the side that writes the foreign key — and
make both sides consistent in memory; mapping ownership differs from domain ownership.
- Decide identity per concept, not per table. Whether something is a dependent child,
an embedded value or an entity in its own right is a domain question with a schema
consequence.
- Ask of every value-shaped type whether it will ever be queried, indexed or reported
on. Compare portability, constraints, update granularity and database JSON support;
querying alone does not decide embedded columns versus JSON.
- Predict and inspect the write statements for a scalar edit, collection addition and
removal. Distinguish child-row deletes, link-table recreation and order-column updates;
a Java
List alone does not predict the SQL.
- Check nullability and constraints follow the model. A mapping that requires
nullable columns for values the domain says are mandatory has moved an invariant out of
the database and into hope.
Decision rules
Identifier for a normal entity
→ a database sequence with an allocation size, or a UUID (v7 or
another time-ordered form) if identity must exist before insert
or across systems. IDENTITY/auto-increment can constrain batching
and generated-key retrieval depending on database, driver and ORM
version—measure the actual insert path for a high-volume table.
Identity must be known before the row exists (event id, correlation,
client-generated)
→ assigned identifier, generated in the constructor. Simplifies
equals/hashCode enormously (orm-behavioral-patterns).
Natural key that is stable, small and never changes
→ usable, and it removes a join in some cases. Rare: most
"natural" keys change eventually. Prefer a surrogate key plus a
unique constraint on the natural one.
Two entities, one reference
→ foreign key mapping. Identify the owning mapping attribute.
Bidirectional is a convenience; keep both sides in sync in a
relationship helper, usually on the aggregate root.
Many-to-many with nothing else to say
→ association table mapping, no entity class.
Many-to-many where the link has a domain attribute (when, by whom, quantity,
role) or independent lifecycle
→ an entity. Retrofitting this later is a migration; the cost of
starting with it includes identity, lifecycle and repository/query cost.
A child that is never referenced from outside its parent and dies with it
→ dependent mapping: choose cascades and orphan removal to enforce
the write lifecycle. Read projections need not create a child write repository.
A value with no identity: Money, Address, DateRange, Coordinates
→ embedded value, columns in the owner's table. Prefer this to
three loose primitives.
A structure that is genuinely opaque to the database: a rendered
document, an audit snapshot, a third-party payload, a variable form
→ serialized LOB / JSON column. Accept vendor-specific query/index
support, coarse update semantics and harder relational constraints/migrations.
The same structure is later needed in a WHERE clause or a report
→ compare relational promotion with supported JSON query/index options.
Choose from constraints, query plans and migration costs.
Rules
- The owning side is the mapping attribute that controls the foreign key/join table, which is
not always the object residing in the table that physically stores the FK. Only changes to the
owning mapping update that relationship. Inverse collection changes can still cascade
persistence or orphan removal, so an inconsistent graph may cause missing links or
constraint failures, not simply a no-op. Keep both sides consistent through one helper.
- Bidirectional associations are a cost: two references to keep in step, two ways to load,
and a serialisation cycle. Map an association bidirectionally only when both traversal
directions are actually used.
- A many-to-many link with domain attributes or lifecycle is usually clearer as an association entity. The association table
becomes an entity with its own identity, and code that treated it as a set must change.
Do not introduce it solely because an attribute might someday appear; use actual lifecycle,
querying and evolution requirements.
- Collection SQL depends on ownership, entity versus value elements, row identifiers and
order semantics. Inspect SQL before replacing a
List with a Set; preserve required
duplicates and ordering. Mutate managed collections through an identity-based diff,
not an automatic clear-and-repopulate operation.
- Embedded values remove loose primitives without introducing independent entity identity.
Record embeddables require a supporting ORM/API version; Java record syntax alone is
insufficient. A basic single-valued embed typically uses owner columns; collection or
secondary-table mappings have different storage shapes.
- Nullability of embedded values is subtle: if every column is null, the ORM may hand back
an object with null fields or a null object depending on version and configuration. If
the value is optional, decide and test which.
- Distinguish opaque LOBs from native JSON. JSON can have database queries, indexes,
validation and SQL migrations. Compare those capabilities with relational constraints,
portability and actual plans. Whole-value ORM writes need concurrency protection;
logical JSON path updates do not imply physically independent writes.
- Dependent mapping concerns write lifecycle. A child entity can have an id and appear in
read-only reports while mutations remain governed by its parent. Independent mutation
commands or reassignment requirements warrant revisiting the boundary
(
repository-pattern).
- Constraints belong in the schema. A mapping that produces nullable columns for mandatory
values, or that omits a unique constraint the domain relies on, has moved enforcement to
application code, where a bulk import will bypass it (
domain-logic-organization).
- Every one of these decisions is a migration once data exists. Spend proportionate analysis now
(
architecture-decision-making).
Before choosing version-sensitive mappings, inspect the Java toolchain, persistence API,
provider, enhancement settings, dialect and schema migrations. Examples are partial mappings,
not standalone applications. Return the selected ownership/lifecycle, expected SQL and
constraints, plus a flush-clear-reload test that would expose the specific defect. Missing
provider or schema evidence means the recommendation remains conditional; do not upgrade
the project to make an example work.
References
- Identity and associations — identifier
generation strategies with their batching, sharding and equality consequences; owning
versus inverse sides with the silent no-op; association table mapping and the moment it
becomes an entity; collection mapping and delete-then-insert; and the query cost of each
shape. Read when mapping a relationship or choosing a key.
- Embedding and serialisation — embedded
values as records, converters for single-column types, dependent mapping and its
lifecycle rules, serialized LOB with the JSON-column trade-off stated honestly, indexing
and migrating JSON, and how to promote a LOB to columns when the requirement changes.
Read when mapping a value type or considering a JSON column.
1---2name: orm-structural-mapping3description: Mapping the structure of an object model onto tables: Identity Field, Foreign Key Mapping, Association Table Mapping, Dependent Mapping, Embedded Value and Serialized LOB. Use when choosing an identifier strategy or when a generated identity breaks batching, when a bidirectional association updates the wrong side and no foreign key is written, when a many-to-many link already has attributes, when child rows are given repositories of their own, when a value type is flattened into columns or hidden in a JSON column, or when a collection is deleted and reinserted on every save. Does not cover subtype mapping (inheritance-mapping-strategies), where the mapping lives (metadata-mapping), runtime fetch behaviour (orm-behavioral-patterns), or which data-access pattern to use in the first place (data-source-patterns).4---56# ORM Structural Mapping78## Purpose910Get the structural mapping decisions right at the point where they are cheap, because each11one becomes a data migration afterwards. These patterns are where the impedance mismatch12actually lives: an object has references, a table has foreign keys; an object may contain a13value, a table has columns; an object graph has ownership, a schema has constraints.1415The failure this prevents is mapping by autocompletion — `@ManyToMany` because there are two16collections, `@GeneratedValue` because it was in the tutorial, a JSON column because the17shape was awkward — and discovering the consequences at production volume.1819## The patterns2021```text22Identity Field the object carries the row's primary key, so the23 mapper can find the row again. Choice of key type24 and generation is a schema-level commitment.2526Foreign Key Mapping an object reference becomes a foreign key column.27 The owning side writes it; the other side is a view.2829Association Table a many-to-many becomes a third table. The moment30Mapping that table needs an attribute, it is an entity.3132Dependent Mapping a child's write lifecycle belongs to its parent;33 an entity child still has an identifier.3435Embedded Value a value object typically becomes owner-table columns.36 No independent persistent identity or lifecycle.3738Serialized LOB a graph is stored as one JSON/XML/binary column.39 Opaque binary and native queryable JSON have different costs.40```4142## Workflow43441. **Choose identity with lifecycle and storage topology**, because generation constrains batching,45 sharding and when equality can be stable. It need not precede every domain decision.462. **For each association, name the owner** — the side that writes the foreign key — and47 make both sides consistent in memory; mapping ownership differs from domain ownership.483. **Decide identity per concept, not per table.** Whether something is a dependent child,49 an embedded value or an entity in its own right is a domain question with a schema50 consequence.514. **Ask of every value-shaped type whether it will ever be queried, indexed or reported52 on.** Compare portability, constraints, update granularity and database JSON support;53 querying alone does not decide embedded columns versus JSON.545. **Predict and inspect the write statements** for a scalar edit, collection addition and55 removal. Distinguish child-row deletes, link-table recreation and order-column updates;56 a Java `List` alone does not predict the SQL.576. **Check nullability and constraints follow the model.** A mapping that requires58 nullable columns for values the domain says are mandatory has moved an invariant out of59 the database and into hope.6061## Decision rules6263```text64Identifier for a normal entity65 → a database sequence with an allocation size, or a UUID (v7 or66 another time-ordered form) if identity must exist before insert67 or across systems. IDENTITY/auto-increment can constrain batching68 and generated-key retrieval depending on database, driver and ORM69 version—measure the actual insert path for a high-volume table.7071Identity must be known before the row exists (event id, correlation,72client-generated)73 → assigned identifier, generated in the constructor. Simplifies74 equals/hashCode enormously (orm-behavioral-patterns).7576Natural key that is stable, small and never changes77 → usable, and it removes a join in some cases. Rare: most78 "natural" keys change eventually. Prefer a surrogate key plus a79 unique constraint on the natural one.8081Two entities, one reference82 → foreign key mapping. Identify the owning mapping attribute.83 Bidirectional is a convenience; keep both sides in sync in a84 relationship helper, usually on the aggregate root.8586Many-to-many with nothing else to say87 → association table mapping, no entity class.8889Many-to-many where the link has a domain attribute (when, by whom, quantity,90role) or independent lifecycle91 → an entity. Retrofitting this later is a migration; the cost of92 starting with it includes identity, lifecycle and repository/query cost.9394A child that is never referenced from outside its parent and dies with it95 → dependent mapping: choose cascades and orphan removal to enforce96 the write lifecycle. Read projections need not create a child write repository.9798A value with no identity: Money, Address, DateRange, Coordinates99 → embedded value, columns in the owner's table. Prefer this to100 three loose primitives.101102A structure that is genuinely opaque to the database: a rendered103document, an audit snapshot, a third-party payload, a variable form104 → serialized LOB / JSON column. Accept vendor-specific query/index105 support, coarse update semantics and harder relational constraints/migrations.106107The same structure is later needed in a WHERE clause or a report108 → compare relational promotion with supported JSON query/index options.109 Choose from constraints, query plans and migration costs.110```111112## Rules113114- **The owning side is the mapping attribute that controls the foreign key/join table**, which is115 not always the object residing in the table that physically stores the FK. Only changes to the116 owning mapping update that relationship. Inverse collection changes can still cascade117 persistence or orphan removal, so an inconsistent graph may cause missing links or118 constraint failures, not simply a no-op. Keep both sides consistent through one helper.119- Bidirectional associations are a cost: two references to keep in step, two ways to load,120 and a serialisation cycle. Map an association bidirectionally only when both traversal121 directions are actually used.122- A many-to-many link with domain attributes or lifecycle is usually clearer as an association entity. The association table123 becomes an entity with its own identity, and code that treated it as a set must change.124 Do not introduce it solely because an attribute might someday appear; use actual lifecycle,125 querying and evolution requirements.126- Collection SQL depends on ownership, entity versus value elements, row identifiers and127 order semantics. Inspect SQL before replacing a `List` with a `Set`; preserve required128 duplicates and ordering. Mutate managed collections through an identity-based diff,129 not an automatic clear-and-repopulate operation.130- Embedded values remove loose primitives without introducing independent entity identity.131 Record embeddables require a supporting ORM/API version; Java record syntax alone is132 insufficient. A basic single-valued embed typically uses owner columns; collection or133 secondary-table mappings have different storage shapes.134- Nullability of embedded values is subtle: if every column is null, the ORM may hand back135 an object with null fields or a null object depending on version and configuration. If136 the value is optional, decide and test which.137- **Distinguish opaque LOBs from native JSON.** JSON can have database queries, indexes,138 validation and SQL migrations. Compare those capabilities with relational constraints,139 portability and actual plans. Whole-value ORM writes need concurrency protection;140 logical JSON path updates do not imply physically independent writes.141- Dependent mapping concerns write lifecycle. A child entity can have an id and appear in142 read-only reports while mutations remain governed by its parent. Independent mutation143 commands or reassignment requirements warrant revisiting the boundary144 (`repository-pattern`).145- Constraints belong in the schema. A mapping that produces nullable columns for mandatory146 values, or that omits a unique constraint the domain relies on, has moved enforcement to147 application code, where a bulk import will bypass it (`domain-logic-organization`).148- Every one of these decisions is a migration once data exists. Spend proportionate analysis now149 (`architecture-decision-making`).150151Before choosing version-sensitive mappings, inspect the Java toolchain, persistence API,152provider, enhancement settings, dialect and schema migrations. Examples are partial mappings,153not standalone applications. Return the selected ownership/lifecycle, expected SQL and154constraints, plus a flush-clear-reload test that would expose the specific defect. Missing155provider or schema evidence means the recommendation remains conditional; do not upgrade156the project to make an example work.157158## References159160- [Identity and associations](references/identity-and-associations.md) — identifier161 generation strategies with their batching, sharding and equality consequences; owning162 versus inverse sides with the silent no-op; association table mapping and the moment it163 becomes an entity; collection mapping and delete-then-insert; and the query cost of each164 shape. Read when mapping a relationship or choosing a key.165- [Embedding and serialisation](references/embedding-and-serialization.md) — embedded166 values as records, converters for single-column types, dependent mapping and its167 lifecycle rules, serialized LOB with the JSON-column trade-off stated honestly, indexing168 and migrating JSON, and how to promote a LOB to columns when the requirement changes.169 Read when mapping a value type or considering a JSON column.