From 196cafa549be05f97307c3eae6590c1cf682eb16 Mon Sep 17 00:00:00 2001 From: Donghwan Kim Date: Thu, 6 Aug 2026 16:22:30 +0900 Subject: [PATCH] Qualify child aggregate columns with the table they are selected from A Criteria or Sort referring to a property of a child aggregate rendered the column against the aggregate root's table, so the generated SQL asked for a column the root table does not have. We now resolve the table from the property path, which qualifies such a column with the joined child table. Closes #2112 Signed-off-by: Donghwan Kim --- .../data/jdbc/core/convert/QueryMapper.java | 35 +++++++++++++++++-- ...JdbcAggregateTemplateIntegrationTests.java | 19 ++++++++++ .../core/convert/SqlGeneratorUnitTests.java | 27 ++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMapper.java index 69c77295e1..d270d631ef 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMapper.java @@ -36,6 +36,7 @@ import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.relational.core.mapping.AggregatePath; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.query.CriteriaDefinition; @@ -57,6 +58,7 @@ * @author Yan Qiang * @author Mikhail Fedorov * @author Christoph Strobl + * @author Donghwan Kim * @since 3.0 */ public class QueryMapper { @@ -127,7 +129,7 @@ private List createSimpleOrderByFields(Table table, @Nullable Rela return fields; } - return List.of(OrderByField.from(table.column(field.getMappedColumnName()))); + return List.of(OrderByField.from(getTable(table, field).column(field.getMappedColumnName()))); } /** @@ -351,7 +353,7 @@ public Object getValue() { } TypeInformation actualType = propertyField.getTypeHint().getRequiredActualType(); - Column column = table.column(propertyField.getMappedColumnName()); + Column column = getTable(table, propertyField).column(propertyField.getMappedColumnName()); Object mappedValue; SQLType sqlType; @@ -469,6 +471,35 @@ private JdbcValue getWriteValue(RelationalPersistentProperty property, Object va ); } + /** + * Returns the {@link Table} a column of the given {@link Field} belongs to. For a property of a child aggregate that + * is the joined table the child is selected from, for everything else the table of the aggregate root. + */ + private Table getTable(Table rootTable, Field field) { + + if (!(field instanceof MetadataBackedField metadataBackedField)) { + return rootTable; + } + + PersistentPropertyPath path = metadataBackedField.getPath(); + + if (path == null) { + return rootTable; + } + + AggregatePath aggregatePath = converter.getMappingContext().getAggregatePath(path); + + // the column of an entity-valued property lives in the owning table, not in the table of the entity itself + if (aggregatePath.isEntity()) { + return rootTable; + } + + AggregatePath.TableInfo tableInfo = aggregatePath.getTableInfo(); + SqlIdentifier tableAlias = tableInfo.tableAlias(); + + return tableAlias == null ? rootTable : Table.create(tableInfo.qualifiedTableName()).as(tableAlias); + } + private Condition mapEmbeddedObjectCondition(CriteriaDefinition criteria, MapSqlParameterSource parameterSource, Table table, RelationalPersistentEntity embeddedEntity, boolean embedded) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java index 088a17ffbb..663b2769e4 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java @@ -88,6 +88,7 @@ * @author Vincent Galloy * @author Sergey Korotaev * @author Sanghun Lee + * @author Donghwan Kim */ @IntegrationTest abstract class AbstractJdbcAggregateTemplateIntegrationTests { @@ -405,6 +406,24 @@ void findAllByQuery() { .containsExactly(tuple(two.id, two.name, 2)); } + @Test // GH-2112 + void findAllByQueryWithCriteriaOnChildAggregate() { + + template.save(createLegoSet("Lava")); + + LegoSet star = createLegoSet("Star"); + star.manual.content = "Assembly instructions for the Star"; + template.save(star); + + Query query = Query.query(Criteria.where("manual.content").is(star.manual.content)); + + Iterable reloadedByManualContent = template.findAll(query, LegoSet.class); + + assertThat(reloadedByManualContent) // + .extracting(l -> l.name) // + .containsExactly("Star"); + } + @Test // GH-1803 void findAllByQueryWithColumns() { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index e1ae69b563..557a2916f5 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -74,6 +74,7 @@ * @author Hari Ohm Prasath * @author Viktor Ardelean * @author Jaeyeon Kim + * @author Donghwan Kim */ @SuppressWarnings("Convert2MethodRef") class SqlGeneratorUnitTests { @@ -465,6 +466,32 @@ void selectByQueryWithMappedColumnPathsRendersCorrectSelection() { "LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1"); } + @Test // GH-2112 + void selectByQueryWithCriteriaOnChildAggregate() { + + Query query = Query.query(Criteria.where("ref.content").is("some content")); + + String sql = sqlGenerator.selectByQuery(query, new MapSqlParameterSource()); + + assertThat(sql).contains( // + "LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1", // + "WHERE ref.x_content = :x_content" // + ); + } + + @Test // GH-2112 + void selectByQuerySortedByChildAggregate() { + + Query query = Query.query(Criteria.where("id").is(23L)).sort(Sort.by("ref.content")); + + String sql = sqlGenerator.selectByQuery(query, new MapSqlParameterSource()); + + assertThat(sql).contains( // + "LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1", // + "ORDER BY ref.x_content ASC" // + ); + } + @Test // GH-1919 void selectBySortedQuery() {