Skip to content

Commit b524be3

Browse files
committed
Add support for composite ids.
Entities may be annotated with `@Id` and `@Embedded`, resulting in a composite id on the database side. The full embedded entity is considered the id, and therefore the check for determining if an aggregate is considered a new aggregate requiring an insert or an existing one, asking for an update is based on that entity, not its elements. Most use cases will require a custom `BeforeConvertCallback` to set the id for new aggregate. For an entity with `@Embedded` id, the back reference used in tables for referenced entities consists of multiple columns, each named by a concatenation of <table-name> + `_` + <column-name>. E.g. the back reference to a `Person` entity, with a composite id with the properties `firstName` and `lastName` will consist of the two columns `PERSON_FIRST_NAME` and `PERSON_LAST_NAME`. This holds for directly referenced entities as well as `List`, `Set` and `Map`. Closes #574 Original pull request #1957
1 parent 5fb0aae commit b524be3

File tree

43 files changed

+2396
-1036
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2396
-1036
lines changed

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

+7-7
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public <T> Object insert(T instance, Class<T> domainType, Identifier identifier,
117117
public <T> Object[] insert(List<InsertSubject<T>> insertSubjects, Class<T> domainType, IdValueSource idValueSource) {
118118

119119
Assert.notEmpty(insertSubjects, "Batch insert must contain at least one InsertSubject");
120+
120121
SqlIdentifierParameterSource[] sqlParameterSources = insertSubjects.stream()
121122
.map(insertSubject -> sqlParametersFactory.forInsert( //
122123
insertSubject.getInstance(), //
@@ -167,7 +168,7 @@ public <S> boolean updateWithVersion(S instance, Class<S> domainType, Number pre
167168
public void delete(Object id, Class<?> domainType) {
168169

169170
String deleteByIdSql = sql(domainType).getDeleteById();
170-
SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER);
171+
SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType);
171172

172173
operations.update(deleteByIdSql, parameter);
173174
}
@@ -188,7 +189,7 @@ public <T> void deleteWithVersion(Object id, Class<T> domainType, Number previou
188189

189190
RelationalPersistentEntity<T> persistentEntity = getRequiredPersistentEntity(domainType);
190191

191-
SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER);
192+
SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forQueryById(id, domainType);
192193
parameterSource.addValue(VERSION_SQL_PARAMETER, previousVersion);
193194
int affectedRows = operations.update(sql(domainType).getDeleteByIdAndVersion(), parameterSource);
194195

@@ -208,8 +209,7 @@ public void delete(Object rootId, PersistentPropertyPath<RelationalPersistentPro
208209

209210
String delete = sql(rootEntity.getType()).createDeleteByPath(propertyPath);
210211

211-
SqlIdentifierParameterSource parameters = sqlParametersFactory.forQueryById(rootId, rootEntity.getType(),
212-
ROOT_ID_PARAMETER);
212+
SqlIdentifierParameterSource parameters = sqlParametersFactory.forQueryById(rootId, rootEntity.getType());
213213
operations.update(delete, parameters);
214214
}
215215

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

245245
String acquireLockByIdSql = sql(domainType).getAcquireLockById(lockMode);
246-
SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER);
246+
SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType);
247247

248248
operations.query(acquireLockByIdSql, parameter, ResultSet::next);
249249
}
@@ -269,7 +269,7 @@ public long count(Class<?> domainType) {
269269
public <T> T findById(Object id, Class<T> domainType) {
270270

271271
String findOneSql = sql(domainType).getFindOne();
272-
SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER);
272+
SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType);
273273

274274
try {
275275
return operations.queryForObject(findOneSql, parameter, getEntityRowMapper(domainType));
@@ -355,7 +355,7 @@ public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
355355
public <T> boolean existsById(Object id, Class<T> domainType) {
356356

357357
String existsSql = sql(domainType).getExists();
358-
SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER);
358+
SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType);
359359

360360
Boolean result = operations.queryForObject(existsSql, parameter, Boolean.class);
361361
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

-54
This file was deleted.

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

+33-8
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

+38-30
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

+11-6
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)