Skip to content

Hibernate 7: write the rows of lists and maps that share a join table - #16756

Open
borinquenkid wants to merge 3 commits into
fix/h7-db2-8.0.xfrom
fix/h7-shared-join-table-lists-8.0.x
Open

borinquenkid wants to merge 3 commits into
fix/h7-db2-8.0.xfrom
fix/h7-shared-join-table-lists-8.0.x

Conversation

@borinquenkid

Copy link
Copy Markdown
Member

Fixes #16700. Stacked on #16750 (it builds on the shared join table uniqueness of #16701 and the Db2 rules of #16750); retarget to 8.0.x once those merge.

Problem

Two unidirectional hasMany lists (or maps) to the same class that share one join table could not be saved:

NULL not allowed for column "ROLES_IDX"; SQL statement:
insert into owner_role (owner_deny_roles_id, deny_roles_idx, role_id) values (?, ?, ?)

Hibernate keys the join table of a list or a map with a primary key over its key and index columns (IndexedCollection.createPrimaryKey). On a shared table the next collection replaces it with its own, and it makes those columns NOT NULL, while a row of one list leaves the other list's columns null.

Change

SharedJoinTableUniqueKeyBinder, for each list or map of a shared table: makes its key and index columns nullable, drops that primary key, and gives it a uniqueness of its own over its key and index columns through SharedJoinTableUniqueness, the same per-database rules sets get:

Database Two lists with a key column each Two lists with one shared key column
H2, PostgreSQL, MySQL, MariaDB a unique key per list a unique key per list
Oracle, Db2 LUW a unique key per list one unique key over the key column and both index columns
Db2 for IBM i and z/OS — a create unique where not null index per list
SQL Server — a filtered unique index per list

A position or a map key is taken once; an element may repeat, as a list allows. On Oracle and Db2 LUW SharedJoinTableUniqueness now creates the key when there is none to widen, since a list has no implicit unique key (a set always has one, so sets are unchanged).

Tests

  • SharedJoinTableListHibernate7Spec (new): both shapes keep their lists in order; removing from one list keeps the other and the order of the rest; an element may repeat and clearing one list keeps the other; two maps keep their entries. A container feature writes and reads both shapes on H2, PostgreSQL, MySQL, MariaDB, Oracle and Db2 (CI, amd64), and checks that a second row at a taken position is rejected; with the uniqueness removed every database accepts it.
  • SharedJoinTableUniqueKeyBinderSpec: the DDL of two shared lists on H2, Oracle, Db2 LUW, IBM i and SQL Server (55/0).
  • Full grails-data-hibernate7-core suite locally: 4293 tests, 0 failures (the shared DatabaseContainers of Hibernate 7: support Db2 in unique key null handling, regular expressions and native SQL reading #16750 makes the Oracle rows pass locally too).

Not covered: a set that shares a join table with a list.

Docs

The shared join table note of the 8.0.x upgrade guide, with the migration a schema created by an earlier Grails 8 release needs (drop the primary key, make the index columns nullable; dbCreate: update changes neither), and the one-to-many chapter.

https://claude.ai/code/session_01MDHn7fC3hsGJ27vFHD2o1p

…able

Two unidirectional hasMany lists (or maps) to the same class that share a
join table could not be saved (#16700). Hibernate keys
the join table of a list or a map with a primary key over its key and
index columns (IndexedCollection.createPrimaryKey), which the next
collection of a shared table replaces with its own and which makes those
columns not null; a row of one list leaves the other list's columns null,
so every insert but the last list's failed with "NULL not allowed for
column ROLES_IDX".

SharedJoinTableUniqueKeyBinder now gives each list or map of a shared
table nullable key and index columns, drops that primary key, and a
uniqueness of its own over its key and index columns, in the shape
SharedJoinTableUniqueness gives sets: a unique key per list, or on Oracle
and Db2 LUW one key over the columns of every list that shares a column
with it. A position or a map key is taken once, while an element may
repeat. SharedJoinTableUniqueness on those databases creates the key when
there is none to widen, as a list has no implicit unique key.

SharedJoinTableListHibernate7Spec: both shapes of shared table keep their
lists in order; removing from one list keeps the other; an element may
repeat and clearing one list keeps the other; two maps keep their
entries; and on H2, PostgreSQL, MySQL, MariaDB, Oracle and Db2 (CI) both
shapes are written and read, and a second row at a taken position is
rejected (without the uniqueness every database accepts it).
SharedJoinTableUniqueKeyBinderSpec: the DDL of two shared lists on H2,
Oracle, Db2 LUW, IBM i and SQL Server. Docs: the upgrade note, with the
migration an older Grails 8 schema needs, and the one-to-many chapter.

Claude-Session: https://claude.ai/code/session_01MDHn7fC3hsGJ27vFHD2o1p

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@borinquenkid
borinquenkid marked this pull request as draft October 24, 2026 21:02
@borinquenkid
borinquenkid marked this pull request as ready for review October 24, 2026 21:02
@borinquenkid

Copy link
Copy Markdown
Member Author

CI run 40132431052 is green, with its tests run (run ci plus a ready toggle). SharedJoinTableListHibernate7Spec's container row passed on H2, Postgres, MySQL, MariaDB, Oracle and Db2 in all three Hibernate 7 test jobs that ran it.

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not approving. Lists and maps that share a table can be inserted, but two lists that also share the element column still load and delete each other's rows.

…in tables

SharedJoinTableUniqueKeyBinder restricts each list and map of a shared join table
by its index column, and unshared element columns if any, ensuring that lists
and maps sharing both foreign-key and element columns isolate their rows during
reads, single-element removals, and bulk deletes. Updated documentation in
the one-to-many chapter and the 8.0 upgrade guide.
@borinquenkid

Copy link
Copy Markdown
Member Author

Resolved the row collision when collections share both foreign-key and element columns:

  • For lists and maps sharing join tables, SharedJoinTableUniqueKeyBinder restricts each collection using its index column (and unshared element columns, if any). This ensures that collections sharing foreign-key and element columns isolate their rows by their distinct index columns (roles_idx is not null vs deny_roles_idx is not null).
  • Loads, single-element removals, and bulk deletes now affect only the target collection's rows.
  • Added tests verifying element ordering, single-element removal, and list-clearing isolation when two lists share both key and element columns.
  • Updated documentation in the one-to-many chapter and the 8.0 upgrade guide.

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The earlier comment is fixed. Each shared list is restricted by its own index column, clearing one keeps the other, and loading no longer selects the other list's rows. That thread is resolved. Not approving yet, only because the migration steps leave an existing table unable to take rows from the other list.

Comment thread grails-doc/src/en/guide/upgrading/upgrading80x.adoc Outdated
…n table migration

Clarify that upgrading an existing Grails 8 schema with lists or maps sharing
a join table requires making both the owner-key and index columns nullable after
dropping the primary key.
@testlens-app

testlens-app Bot commented Oct 25, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: a99fd4b
▶️ Tests: 74301 executed
⚪️ Checks: 89/89 completed


Learn more about TestLens at testlens.app/docs.

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The upgrade note now says to drop the primary key and make the owner-key and index columns nullable, which is what the binder does. That comment is resolved.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants