Skip to content

Fix @InsertOnlyProperty handling for embedded properties - #2367

Open
alpin87 wants to merge 1 commit into
spring-projects:mainfrom
alpin87:fix/insert-only-embedded
Open

Fix @InsertOnlyProperty handling for embedded properties#2367
alpin87 wants to merge 1 commit into
spring-projects:mainfrom
alpin87:fix/insert-only-embedded

Conversation

@alpin87

@alpin87 alpin87 commented Aug 21, 2026

Copy link
Copy Markdown

Closes #2366

@InsertOnlyProperty was silently ignored when combined with @Embedded, so insert-only columns (the typical case being creation-audit fields grouped in an embeddable) were overwritten on every update.

What was broken

  • R2dbcEntityTemplate collected insert-only columns by iterating the top-level entity properties. An annotated embedded container contributed its container name (not a real column of the outbound row, which holds the expanded, prefixed leaf columns), and an annotated property inside an embeddable was never visited. Both the update path and the upsert path's insertOnlyColumns collection (added in R2dbcEntityTemplate.upsert() generates invalid SQL for entities with @Embedded.Empty composite primary keys #2313 / fc94989) were affected.

  • On the JDBC side, SqlGenerator handled leaf-annotated properties correctly (its column-cache recursion reads each leaf's isInsertOnly()), but an annotated container was not propagated to the columns it contributes. Verified with a test before the fix:

    UPDATE "WITH_INSERT_ONLY_EMBEDDED" SET "NAME" = :name, "AUDIT_CREATED_BY" = :audit_created_by, "AUDIT_CREATED_AT" = :audit_created_at WHERE ...
    

    So the leaf-annotated gap was R2DBC-specific, while the container-annotated gap affected both modules (also noted on the issue).

Fix

  • R2dbcEntityTemplate: a private helper collects insert-only column names by expanding embedded properties through RelationalMappingContext#getRequiredPersistentEntity(RelationalPersistentProperty) — the same recursion DefaultReactiveDataAccessStrategy#getAllColumns already uses (the cast to RelationalMappingContext mirrors that class as well). Properties inside an insert-only container are treated as insert-only. Used by both the update and the upsert path.
  • SqlGenerator (JDBC): the embedded recursion in the column-name cache now propagates the container's insert-only flag, so leaf columns of an insert-only embedded land in insertOnlyColumnNames and are excluded from the updatable columns.

Tests

Five new tests, all failing before the fix and passing after:

  • R2dbcEntityTemplateUnitTests: update excludes insert-only embedded container columns / insert-only properties inside an embeddable (exact-SQL assertions in the style of updateExcludesInsertOnlyColumns)
  • ReactiveUpsertOperationUnitTests: the DO UPDATE SET clause no longer touches insert-only embedded columns
  • SqlGeneratorUnitTests: JDBC update SQL excludes container-contributed insert-only columns; leaf-annotated exclusion kept as a regression guard

Full spring-data-jdbc and spring-data-r2dbc test suites pass.


  • You have read the Spring Data contribution guidelines.
  • You use the code formatters provided here and have them applied to your changes. Don't submit any formatting related changes.
  • You submit test cases (unit or integration tests) that back your changes.
  • You added yourself as author in the headers of the classes you touched. Amend the date range in the Apache license header if needed. For new types, add the license header (copy from another file and set the current year only).

Insert-only columns contributed by embedded properties were not
excluded from the SET clause. R2dbcEntityTemplate collected
insert-only columns by iterating the top-level entity properties,
so an annotated embedded container contributed its container name
instead of its expanded leaf columns, and annotated properties
inside an embeddable were never visited. The JDBC SqlGenerator
handled leaf-annotated properties but did not propagate an
annotated container to the columns it contributes.

Collect insert-only columns by expanding embedded properties in
R2dbcEntityTemplate, used by both the update and the upsert path,
and propagate the container's insert-only flag through the
embedded recursion in SqlGenerator's column-name cache.

Closes spring-projects#2366

Signed-off-by: seungmin <qortmdals94@naver.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged

Projects

None yet

2 participants