Skip to content

Commit af01a47

Browse files
Handle composite-id AggregateReference SQL generation.
Signed-off-by: wonderfulrosemari <whwlsgur1419@naver.com>
1 parent 8ce50b8 commit af01a47

File tree

6 files changed

+179
-3
lines changed

6 files changed

+179
-3
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,7 @@ static class Columns {
13061306

13071307
Set<SqlIdentifier> insertable = new LinkedHashSet<>(nonIdColumnNames);
13081308
insertable.removeAll(readOnlyColumnNames);
1309+
replaceReferenceColumns(entity, insertable);
13091310

13101311
this.insertableColumns = Collections.unmodifiableSet(insertable);
13111312

@@ -1314,10 +1315,58 @@ static class Columns {
13141315
updatable.removeAll(idColumnNames);
13151316
updatable.removeAll(readOnlyColumnNames);
13161317
updatable.removeAll(insertOnlyColumnNames);
1318+
replaceReferenceColumns(entity, updatable);
13171319

13181320
this.updatableColumns = Collections.unmodifiableSet(updatable);
13191321
}
13201322

1323+
private void replaceReferenceColumns(RelationalPersistentEntity<?> entity, Set<SqlIdentifier> targetColumns) {
1324+
1325+
entity.doWithAll(property -> {
1326+
1327+
Set<SqlIdentifier> referenceColumns = getReferenceIdColumns(property);
1328+
if (referenceColumns.isEmpty()) {
1329+
return;
1330+
}
1331+
1332+
targetColumns.remove(property.getColumnName());
1333+
targetColumns.addAll(referenceColumns);
1334+
});
1335+
}
1336+
1337+
private Set<SqlIdentifier> getReferenceIdColumns(RelationalPersistentProperty property) {
1338+
1339+
if (!property.isAssociation()) {
1340+
return Collections.emptySet();
1341+
}
1342+
1343+
RelationalPersistentEntity<?> referenceIdEntity = mappingContext.getPersistentEntity(converter.getColumnType(property));
1344+
if (referenceIdEntity == null) {
1345+
return Collections.emptySet();
1346+
}
1347+
1348+
Set<SqlIdentifier> columns = new LinkedHashSet<>();
1349+
collectSimpleColumns(referenceIdEntity, property.getEmbeddedPrefix(), columns);
1350+
return columns;
1351+
}
1352+
1353+
private void collectSimpleColumns(RelationalPersistentEntity<?> entity, String prefix, Set<SqlIdentifier> columns) {
1354+
1355+
entity.doWithAll(property -> {
1356+
1357+
if (property.isEmbedded() && property.isEntity()) {
1358+
RelationalPersistentEntity<?> embeddedEntity = mappingContext
1359+
.getRequiredPersistentEntity(converter.getColumnType(property));
1360+
collectSimpleColumns(embeddedEntity, prefix + property.getEmbeddedPrefix(), columns);
1361+
return;
1362+
}
1363+
1364+
if (!property.isEntity()) {
1365+
columns.add(property.getColumnName().transform(prefix::concat));
1366+
}
1367+
});
1368+
}
1369+
13211370
private void populateColumnNameCache(RelationalPersistentEntity<?> entity, String prefix) {
13221371

13231372
entity.doWithAll(property -> {

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.jspecify.annotations.Nullable;
2626

2727
import org.springframework.data.jdbc.core.mapping.JdbcValue;
28+
import org.springframework.data.jdbc.core.mapping.AggregateReference;
2829
import org.springframework.data.jdbc.support.JdbcUtil;
2930
import org.springframework.data.mapping.PersistentProperty;
3031
import org.springframework.data.mapping.PersistentPropertyAccessor;
@@ -252,11 +253,22 @@ private <S, T> ParameterSourceHolder getParameterSource(@Nullable S instance,
252253
return;
253254
}
254255

256+
RelationalPersistentEntity<?> referenceIdEntity = getReferenceIdEntity(property);
257+
if (referenceIdEntity != null) {
258+
259+
Object value = propertyAccessor.getProperty(property);
260+
Object referenceId = value instanceof AggregateReference<?, ?> reference ? reference.getId() : value;
261+
262+
ParameterSourceHolder additionalParameters = getParameterSource((T) referenceId,
263+
(RelationalPersistentEntity<T>) referenceIdEntity, prefix + property.getEmbeddedPrefix(), skipProperty);
264+
holder.addAll(additionalParameters);
265+
return;
266+
}
267+
255268
if (property.isEmbedded()) {
256269

257270
Object value = propertyAccessor.getProperty(property);
258-
RelationalPersistentEntity<?> embeddedEntity = context
259-
.getRequiredPersistentEntity(property.getTypeInformation());
271+
RelationalPersistentEntity<?> embeddedEntity = context.getRequiredPersistentEntity(property);
260272
ParameterSourceHolder additionalParameters = getParameterSource((T) value,
261273
(RelationalPersistentEntity<T>) embeddedEntity, prefix + property.getEmbeddedPrefix(), skipProperty);
262274
holder.addAll(additionalParameters);
@@ -270,6 +282,16 @@ private <S, T> ParameterSourceHolder getParameterSource(@Nullable S instance,
270282
return holder;
271283
}
272284

285+
@Nullable
286+
private RelationalPersistentEntity<?> getReferenceIdEntity(RelationalPersistentProperty property) {
287+
288+
if (!AggregateReference.class.isAssignableFrom(property.getRawType())) {
289+
return null;
290+
}
291+
292+
return context.getPersistentEntity(converter.getColumnType(property));
293+
}
294+
273295
/**
274296
* A {@link PersistentPropertyAccessor} implementation always returning null
275297
*

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,17 @@ void readOnlyPropertyExcludedFromQuery_when_generateInsertSql() {
657657
);
658658
}
659659

660+
@Test // GH-2113
661+
void insertUsesCompositeIdColumnsForAggregateReference() {
662+
663+
SqlGenerator sqlGenerator = createSqlGenerator(WithCompositeIdReference.class, AnsiDialect.INSTANCE);
664+
665+
String insert = sqlGenerator.getInsert(emptySet());
666+
667+
assertThat(insert).contains("\"X_ORGANIZATION\"", "\"X_EMPLOYEE_NUMBER\"");
668+
assertThat(insert).doesNotContain("\"X_EMPLOYEE\"");
669+
}
670+
660671
@Test // DATAJDBC-324
661672
void readOnlyPropertyIncludedIntoQuery_when_generateFindAllSql() {
662673

@@ -1106,6 +1117,20 @@ static class OtherAggregate {
11061117
String name;
11071118
}
11081119

1120+
@SuppressWarnings("unused")
1121+
static class AggregateWithCompositeId {
1122+
@Id CompositeId id;
1123+
}
1124+
1125+
record CompositeId(String organization, Long employeeNumber) {
1126+
}
1127+
1128+
@SuppressWarnings("unused")
1129+
static class WithCompositeIdReference {
1130+
@Id Long id;
1131+
AggregateReference<AggregateWithCompositeId, CompositeId> employee;
1132+
}
1133+
11091134
private static class PrefixingNamingStrategy extends DefaultNamingStrategy {
11101135

11111136
@Override

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryUnitTests.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.data.annotation.Id;
3333
import org.springframework.data.convert.ReadingConverter;
3434
import org.springframework.data.convert.WritingConverter;
35+
import org.springframework.data.jdbc.core.mapping.AggregateReference;
3536
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
3637
import org.springframework.data.relational.core.conversion.IdValueSource;
3738
import org.springframework.data.relational.core.mapping.Column;
@@ -181,6 +182,22 @@ void parametersForInsertForEmbeddedWrappedId() {
181182
});
182183
}
183184

185+
@Test // GH-2113
186+
void parametersForInsertExpandAggregateReferenceCompositeId() {
187+
188+
Locker locker = new Locker(null, AggregateReference.to(new EmployeeId("Engineering", 42L)));
189+
190+
SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forInsert(locker, Locker.class,
191+
Identifier.empty(), IdValueSource.GENERATED);
192+
193+
SoftAssertions.assertSoftly(softly -> {
194+
softly.assertThat(parameterSource.getParameterNames()).contains("organization", "employee_number");
195+
softly.assertThat(parameterSource.getParameterNames()).doesNotContain("employee");
196+
softly.assertThat(parameterSource.getValue("organization")).isEqualTo("Engineering");
197+
softly.assertThat(parameterSource.getValue("employee_number")).isEqualTo(42L);
198+
});
199+
}
200+
184201
@WritingConverter
185202
enum IdValueToStringConverter implements Converter<IdValue, String> {
186203

@@ -331,4 +348,22 @@ private record SingleEmbeddedIdEntity( //
331348
String name //
332349
) {
333350
}
351+
352+
private static class Employee {
353+
@Id EmployeeId id;
354+
}
355+
356+
private record EmployeeId(String organization, Long employeeNumber) {
357+
}
358+
359+
private static class Locker {
360+
361+
@Id Long id;
362+
AggregateReference<Employee, EmployeeId> employee;
363+
364+
private Locker(Long id, AggregateReference<Employee, EmployeeId> employee) {
365+
this.id = id;
366+
this.employee = employee;
367+
}
368+
}
334369
}

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCrossAggregateHsqlIntegrationTests.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public class JdbcRepositoryCrossAggregateHsqlIntegrationTests {
5757
@Configuration
5858
@Import(TestConfiguration.class)
5959
@EnableJdbcRepositories(considerNestedRepositories = true,
60-
includeFilters = @ComponentScan.Filter(value = { Ones.class, ReferencingAggregateRepository.class },
60+
includeFilters = @ComponentScan.Filter(value = { Ones.class, ReferencingAggregateRepository.class, Lockers.class },
6161
type = FilterType.ASSIGNABLE_TYPE))
6262
static class Config {
6363

@@ -70,6 +70,7 @@ JdbcCustomConversions jdbcCustomConversions() {
7070
@Autowired NamedParameterJdbcTemplate template;
7171
@Autowired Ones ones;
7272
@Autowired ReferencingAggregateRepository referencingAggregates;
73+
@Autowired Lockers lockers;
7374
@Autowired RelationalMappingContext context;
7475

7576
@SuppressWarnings("ConstantConditions")
@@ -118,6 +119,19 @@ public void savesAndReadWithConvertableId() {
118119
assertThat(reloaded.ref()).isEqualTo(idReference);
119120
}
120121

122+
@Test // GH-2113
123+
void savesAggregateReferenceWithCompositeId() {
124+
125+
EmployeeId employeeId = new EmployeeId("Engineering", 42L);
126+
template.getJdbcOperations().update(
127+
"INSERT INTO employee (employee_number, organization, name) VALUES (42, 'Engineering', 'Ada')");
128+
129+
lockers.save(new Locker(null, AggregateReference.to(employeeId), 4));
130+
131+
assertThat(JdbcTestUtils.countRowsInTableWhere(template.getJdbcOperations(), "locker",
132+
"employee_number = 42 AND organization = 'Engineering'")).isEqualTo(1);
133+
}
134+
121135
interface Ones extends CrudRepository<AggregateOne, Long> {}
122136

123137
static class AggregateOne {
@@ -137,6 +151,10 @@ interface ReferencingAggregateRepository extends CrudRepository<ReferencingAggre
137151

138152
}
139153

154+
interface Lockers extends CrudRepository<Locker, Long> {
155+
156+
}
157+
140158
record AggregateWithConvertableId(@Id AggregateId id, String name) {
141159

142160
}
@@ -149,6 +167,15 @@ record ReferencingAggregate(@Id Long id, String name,
149167
AggregateReference<AggregateWithConvertableId, AggregateId> ref) {
150168
}
151169

170+
record Employee(@Id EmployeeId id, String name) {
171+
}
172+
173+
record EmployeeId(String organization, Long employeeNumber) {
174+
}
175+
176+
record Locker(@Id Long id, AggregateReference<Employee, EmployeeId> assignedTo, Integer capacity) {
177+
}
178+
152179
@WritingConverter
153180
private enum AggregateIdToLong implements Converter<AggregateId, Long> {
154181

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCrossAggregateHsqlIntegrationTests-hsql.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,21 @@ CREATE TABLE REFERENCING_AGGREGATE
1111
NAME VARCHAR(100),
1212
REF INTEGER
1313
);
14+
15+
CREATE TABLE employee
16+
(
17+
organization VARCHAR(20),
18+
employee_number BIGINT,
19+
name VARCHAR(100),
20+
CONSTRAINT pk_employee PRIMARY KEY (employee_number, organization)
21+
);
22+
23+
CREATE TABLE locker
24+
(
25+
id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
26+
capacity INTEGER,
27+
employee_number BIGINT,
28+
organization VARCHAR(20),
29+
CONSTRAINT fk_locker_employee FOREIGN KEY (employee_number, organization)
30+
REFERENCES employee (employee_number, organization)
31+
);

0 commit comments

Comments
 (0)