Skip to content

[OPENJPA-2990] Remove the rows of the tables owned by bulk deleted entities - #198

Merged
cristof merged 3 commits into
masterfrom
OPENJPA-2990
Oct 1, 2026
Merged

cristof merged 3 commits into
masterfrom
OPENJPA-2990

Conversation

@rzo1

@rzo1 rzo1 commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

A bulk JPQL DELETE deleted only the entity rows and left behind the rows of the tables the entity owns — join tables of uni-directional @OneToMany/@ManyToMany and @ElementCollection tables — dangling on deleted primary keys; cascading to related entities stays absent as the spec requires. The candidate keys are now selected once and the owned rows and entity rows are deleted by key in chunked IN lists, so criteria such as SIZE(), MEMBER OF or a correlated EXISTS cannot be affected by the deletes, while a criteria-less delete skips the key select entirely; composite ids, multi-column join foreign keys and shared container tables with constant join columns fall back to the in-memory path. The new CleanupOwnedTablesOnBulkDelete compatibility option restores the previous behaviour, and the migration guide documents the statement sequence, the fallbacks and the remaining bi-directional join table limitation.

@rzo1 rzo1 self-assigned this Sep 27, 2026
@rzo1
rzo1 requested review from cristof and solomax September 27, 2026 05:32
…tities

A bulk delete does not cascade to related entities, but the rows of the join
tables and element collection tables owned by the deleted entities are not
entities themselves. Since the cascade guard was removed from the JDBC bulk
delete strategy they were left behind, dangling on the primary keys of the
deleted rows.

When the candidate owns such a table, the delete now evaluates the criteria
exactly once into a list of primary keys and deletes by those keys: the owned
tables first, the entity table last. Nothing re-evaluates the criteria against
a database a previous statement has already changed. Candidates with a
composite primary key, or with a join foreign key that is not a single column
referencing that primary key, are executed in memory instead. The
compatibility option CleanupOwnedTablesOnBulkDelete (default true) suppresses
the cleanup.

A delete that carries no criteria at all - no where clause and no
discriminator or subclass condition - matches every candidate row, so every
row of every owned table belongs to a deleted candidate. Such a delete skips
the key select and empties the owned tables outright, which keeps the single
statement delete of a whole table a single statement per table.

The table of a join foreign key is only taken for an owned table if it is
neither a table of the key type nor one of the element type or of any of their
joined superclasses, so an inverse key mapping never turns into a delete of
entity rows. An embedded value has no table of its own - the table of an
embeddable is the table it is embedded into - so the collection table of an
element collection of embeddables is not mistaken for the table of a related
entity.

A join foreign key that carries constant columns discriminates the rows of a
table that is shared by more than one mapping, so the candidates do not own
every row of it: neither the key delete nor the delete without criteria is
applicable and such a candidate is executed in memory.
if (isUnfiltered(sel) && !hasConstantJoin(joins)) {
// every row of every owned table belongs to a deleted
// candidate, so there is nothing to materialize
if (owned == null)

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.

I would use {} everywhere :)

@cristof
cristof merged commit 4ea05d8 into master Oct 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants