Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 599a7d7

Browse files
committedDec 11, 2024··
Add support for composite ids.
It is now possible to use composite ids, by having an entity reference annotated with `@Id` and `@Embedded`. Closes #574
1 parent d0b8021 commit 599a7d7

File tree

38 files changed

+2309
-1021
lines changed

38 files changed

+2309
-1021
lines changed
 

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ public <T> Object insert(T instance, Class<T> domainType, Identifier identifier,
115115
public <T> Object[] insert(List<InsertSubject<T>> insertSubjects, Class<T> domainType, IdValueSource idValueSource) {
116116

117117
Assert.notEmpty(insertSubjects, "Batch insert must contain at least one InsertSubject");
118+
118119
SqlIdentifierParameterSource[] sqlParameterSources = insertSubjects.stream()
119120
.map(insertSubject -> sqlParametersFactory.forInsert(insertSubject.getInstance(), domainType,
120121
insertSubject.getIdentifier(), idValueSource))
@@ -160,7 +161,7 @@ public <S> boolean updateWithVersion(S instance, Class<S> domainType, Number pre
160161
public void delete(Object id, Class<?> domainType) {
161162

162163
String deleteByIdSql = sql(domainType).getDeleteById();
163-
SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER);
164+
SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType);
164165

165166
operations.update(deleteByIdSql, parameter);
166167
}
@@ -181,7 +182,7 @@ public <T> void deleteWithVersion(Object id, Class<T> domainType, Number previou
181182

182183
RelationalPersistentEntity<T> persistentEntity = getRequiredPersistentEntity(domainType);
183184

184-
SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER);
185+
SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forQueryById(id, domainType);
185186
parameterSource.addValue(VERSION_SQL_PARAMETER, previousVersion);
186187
int affectedRows = operations.update(sql(domainType).getDeleteByIdAndVersion(), parameterSource);
187188

@@ -201,8 +202,7 @@ public void delete(Object rootId, PersistentPropertyPath<RelationalPersistentPro
201202

202203
String delete = sql(rootEntity.getType()).createDeleteByPath(propertyPath);
203204

204-
SqlIdentifierParameterSource parameters = sqlParametersFactory.forQueryById(rootId, rootEntity.getType(),
205-
ROOT_ID_PARAMETER);
205+
SqlIdentifierParameterSource parameters = sqlParametersFactory.forQueryById(rootId, rootEntity.getType());
206206
operations.update(delete, parameters);
207207
}
208208

@@ -236,7 +236,7 @@ public void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> prope
236236
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
237237

238238
String acquireLockByIdSql = sql(domainType).getAcquireLockById(lockMode);
239-
SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER);
239+
SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType);
240240

241241
operations.query(acquireLockByIdSql, parameter, ResultSet::next);
242242
}
@@ -262,7 +262,7 @@ public long count(Class<?> domainType) {
262262
public <T> T findById(Object id, Class<T> domainType) {
263263

264264
String findOneSql = sql(domainType).getFindOne();
265-
SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER);
265+
SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType);
266266

267267
try {
268268
return operations.queryForObject(findOneSql, parameter, getEntityRowMapper(domainType));
@@ -329,7 +329,7 @@ public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
329329
public <T> boolean existsById(Object id, Class<T> domainType) {
330330

331331
String existsSql = sql(domainType).getExists();
332-
SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER);
332+
SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType);
333333

334334
Boolean result = operations.queryForObject(existsSql, parameter, Boolean.class);
335335
Assert.state(result != null, "The result of an exists query must not be null");

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

Lines changed: 0 additions & 54 deletions
This file was deleted.

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

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515
*/
1616
package org.springframework.data.jdbc.core.convert;
1717

18+
import java.util.function.Function;
19+
20+
import org.springframework.data.mapping.PersistentPropertyPathAccessor;
1821
import org.springframework.data.relational.core.mapping.AggregatePath;
22+
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
23+
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
24+
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
1925
import org.springframework.util.Assert;
2026

2127
/**
@@ -41,13 +47,31 @@ public static JdbcIdentifierBuilder empty() {
4147
*/
4248
public static JdbcIdentifierBuilder forBackReferences(JdbcConverter converter, AggregatePath path, Object value) {
4349

44-
Identifier identifier = Identifier.of( //
45-
path.getTableInfo().reverseColumnInfo().name(), //
46-
value, //
47-
converter.getColumnType(path.getIdDefiningParentPath().getRequiredIdProperty()) //
48-
);
50+
RelationalPersistentProperty idProperty = path.getIdDefiningParentPath().getRequiredIdProperty();
51+
AggregatePath.ColumnInfos reverseColumnInfos = path.getTableInfo().reverseColumnInfos();
52+
53+
// create property accessor
54+
RelationalMappingContext mappingContext = converter.getMappingContext();
55+
RelationalPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(idProperty.getType());
56+
57+
Function<AggregatePath, Object> valueProvider;
58+
if (persistentEntity == null) {
59+
valueProvider = ap -> value;
60+
} else {
61+
PersistentPropertyPathAccessor<Object> propertyPathAccessor = persistentEntity.getPropertyPathAccessor(value);
62+
valueProvider = ap -> propertyPathAccessor.getProperty(ap.getRequiredPersistentPropertyPath());
63+
}
64+
65+
final Identifier[] identifierHolder = new Identifier[] { Identifier.empty() };
66+
67+
reverseColumnInfos.forEach((ap, ci) -> {
68+
69+
RelationalPersistentProperty property = ap.getRequiredLeafProperty();
70+
identifierHolder[0] = identifierHolder[0].withPart(ci.name(), valueProvider.apply(ap),
71+
converter.getColumnType(property));
72+
});
4973

50-
return new JdbcIdentifierBuilder(identifier);
74+
return new JdbcIdentifierBuilder(identifierHolder[0]);
5175
}
5276

5377
/**
@@ -62,8 +86,9 @@ public JdbcIdentifierBuilder withQualifier(AggregatePath path, Object value) {
6286
Assert.notNull(path, "Path must not be null");
6387
Assert.notNull(value, "Value must not be null");
6488

65-
identifier = identifier.withPart(path.getTableInfo().qualifierColumnInfo().name(), value,
66-
path.getTableInfo().qualifierColumnType());
89+
AggregatePath.TableInfo tableInfo = path.getTableInfo();
90+
identifier = identifier.withPart(tableInfo.qualifierColumnInfo().name(), value,
91+
tableInfo.qualifierColumnType());
6792

6893
return this;
6994
}

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

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements
8080
* {@link #MappingJdbcConverter(RelationalMappingContext, RelationResolver, CustomConversions, JdbcTypeFactory)}
8181
* (MappingContext, RelationResolver, JdbcTypeFactory)} to convert arrays and large objects into JDBC-specific types.
8282
*
83-
* @param context must not be {@literal null}.
83+
* @param context must not be {@literal null}.
8484
* @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}.
8585
*/
8686
public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) {
@@ -98,12 +98,12 @@ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver r
9898
/**
9999
* Creates a new {@link MappingJdbcConverter} given {@link MappingContext}.
100100
*
101-
* @param context must not be {@literal null}.
101+
* @param context must not be {@literal null}.
102102
* @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}.
103-
* @param typeFactory must not be {@literal null}
103+
* @param typeFactory must not be {@literal null}
104104
*/
105105
public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver,
106-
CustomConversions conversions, JdbcTypeFactory typeFactory) {
106+
CustomConversions conversions, JdbcTypeFactory typeFactory) {
107107

108108
super(context, conversions);
109109

@@ -220,7 +220,7 @@ private boolean canWriteAsJdbcValue(@Nullable Object value) {
220220
return true;
221221
}
222222

223-
if (value instanceof AggregateReference aggregateReference) {
223+
if (value instanceof AggregateReference<?, ?> aggregateReference) {
224224
return canWriteAsJdbcValue(aggregateReference.getId());
225225
}
226226

@@ -285,7 +285,7 @@ public <R> R readAndResolve(TypeInformation<R> type, RowDocument source, Identif
285285

286286
@Override
287287
protected RelationalPropertyValueProvider newValueProvider(RowDocumentAccessor documentAccessor,
288-
ValueExpressionEvaluator evaluator, ConversionContext context) {
288+
ValueExpressionEvaluator evaluator, ConversionContext context) {
289289

290290
if (context instanceof ResolvingConversionContext rcc) {
291291

@@ -314,7 +314,7 @@ class ResolvingRelationalPropertyValueProvider implements RelationalPropertyValu
314314
private final Identifier identifier;
315315

316316
private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider delegate, RowDocumentAccessor accessor,
317-
ResolvingConversionContext context, Identifier identifier) {
317+
ResolvingConversionContext context, Identifier identifier) {
318318

319319
AggregatePath path = context.aggregatePath();
320320

@@ -323,15 +323,15 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele
323323
this.context = context;
324324
this.identifier = path.isEntity()
325325
? potentiallyAppendIdentifier(identifier, path.getRequiredLeafEntity(),
326-
property -> delegate.getValue(path.append(property)))
326+
property -> delegate.getValue(path.append(property)))
327327
: identifier;
328328
}
329329

330330
/**
331331
* Conditionally append the identifier if the entity has an identifier property.
332332
*/
333333
static Identifier potentiallyAppendIdentifier(Identifier base, RelationalPersistentEntity<?> entity,
334-
Function<RelationalPersistentProperty, Object> getter) {
334+
Function<RelationalPersistentProperty, Object> getter) {
335335

336336
if (entity.hasIdProperty()) {
337337

@@ -361,24 +361,9 @@ public <T> T getPropertyValue(RelationalPersistentProperty property) {
361361

362362
if (property.isCollectionLike() || property.isMap()) {
363363

364-
Identifier identifierToUse = this.identifier;
365-
AggregatePath idDefiningParentPath = aggregatePath.getIdDefiningParentPath();
364+
Identifier identifier = constructIdentifier(aggregatePath);
366365

367-
// note that the idDefiningParentPath might not itself have an id property, but have a combination of back
368-
// references and possibly keys, that form an id
369-
if (idDefiningParentPath.hasIdProperty()) {
370-
371-
RelationalPersistentProperty identifier = idDefiningParentPath.getRequiredIdProperty();
372-
AggregatePath idPath = idDefiningParentPath.append(identifier);
373-
Object value = delegate.getValue(idPath);
374-
375-
Assert.state(value != null, "Identifier value must not be null at this point");
376-
377-
identifierToUse = Identifier.of(aggregatePath.getTableInfo().reverseColumnInfo().name(), value,
378-
identifier.getActualType());
379-
}
380-
381-
Iterable<Object> allByPath = relationResolver.findAllByPath(identifierToUse,
366+
Iterable<Object> allByPath = relationResolver.findAllByPath(identifier,
382367
aggregatePath.getRequiredPersistentPropertyPath());
383368

384369
if (property.isCollectionLike()) {
@@ -403,6 +388,29 @@ public <T> T getPropertyValue(RelationalPersistentProperty property) {
403388
return (T) delegate.getValue(aggregatePath);
404389
}
405390

391+
private Identifier constructIdentifier(AggregatePath aggregatePath) {
392+
393+
Identifier identifierToUse = this.identifier;
394+
AggregatePath idDefiningParentPath = aggregatePath.getIdDefiningParentPath();
395+
396+
// note that the idDefiningParentPath might not itself have an id property, but have a combination of back
397+
// references and possibly keys, that form an id
398+
if (idDefiningParentPath.hasIdProperty()) {
399+
400+
RelationalPersistentProperty idProperty = idDefiningParentPath.getRequiredIdProperty();
401+
AggregatePath idPath = idProperty.isEntity() ? idDefiningParentPath.append(idProperty) : idDefiningParentPath;
402+
Identifier[] buildingIdentifier = new Identifier[] { Identifier.empty() };
403+
aggregatePath.getTableInfo().reverseColumnInfos().forEach((ap, ci) -> {
404+
405+
Object value = delegate.getValue(idPath.append(ap));
406+
buildingIdentifier[0] = buildingIdentifier[0].withPart(ci.name(), value,
407+
ap.getRequiredLeafProperty().getActualType());
408+
});
409+
identifierToUse = buildingIdentifier[0];
410+
}
411+
return identifierToUse;
412+
}
413+
406414
@Override
407415
public boolean hasValue(RelationalPersistentProperty property) {
408416

@@ -423,7 +431,7 @@ public boolean hasValue(RelationalPersistentProperty property) {
423431
return delegate.hasValue(toUse);
424432
}
425433

426-
return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfo().alias());
434+
return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfos().any().alias());
427435
}
428436

429437
return delegate.hasValue(aggregatePath);
@@ -449,7 +457,7 @@ public boolean hasNonEmptyValue(RelationalPersistentProperty property) {
449457
return delegate.hasValue(toUse);
450458
}
451459

452-
return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfo().alias());
460+
return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfos().any().alias());
453461
}
454462

455463
return delegate.hasNonEmptyValue(aggregatePath);
@@ -460,7 +468,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) {
460468

461469
return context == this.context ? this
462470
: new ResolvingRelationalPropertyValueProvider(delegate.withContext(context), accessor,
463-
(ResolvingConversionContext) context, identifier);
471+
(ResolvingConversionContext) context, identifier);
464472
}
465473
}
466474

@@ -472,7 +480,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) {
472480
* @param identifier
473481
*/
474482
private record ResolvingConversionContext(ConversionContext delegate, AggregatePath aggregatePath,
475-
Identifier identifier) implements ConversionContext {
483+
Identifier identifier) implements ConversionContext {
476484

477485
@Override
478486
public <S> S convert(Object source, TypeInformation<? extends S> typeHint) {

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ class SqlContext {
4040
this.table = Table.create(entity.getQualifiedTableName());
4141
}
4242

43-
Column getIdColumn() {
44-
return table.column(entity.getIdColumn());
45-
}
46-
4743
Column getVersionColumn() {
4844
return table.column(entity.getRequiredVersionProperty().getColumnName());
4945
}
@@ -60,11 +56,20 @@ Table getTable(AggregatePath path) {
6056
}
6157

6258
Column getColumn(AggregatePath path) {
59+
6360
AggregatePath.ColumnInfo columnInfo = path.getColumnInfo();
6461
return getTable(path).column(columnInfo.name()).as(columnInfo.alias());
6562
}
6663

67-
Column getReverseColumn(AggregatePath path) {
68-
return getTable(path).column(path.getTableInfo().reverseColumnInfo().name()).as(path.getTableInfo().reverseColumnInfo().alias());
64+
/**
65+
* A token reverse column, used in selects to identify, if an entity is present or {@literal null}.
66+
*
67+
* @param path must not be null.
68+
* @return a {@literal Column} that is part of the effective primary key for the given path.
69+
*/
70+
Column getAnyReverseColumn(AggregatePath path) {
71+
72+
AggregatePath.ColumnInfo columnInfo = path.getTableInfo().reverseColumnInfos().any();
73+
return getTable(path).column(columnInfo.name()).as(columnInfo.alias());
6974
}
7075
}

0 commit comments

Comments
 (0)