Skip to content

Add support for composite ids #1957

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
83236b2
Prepare next development iteration.
mp911de Nov 19, 2024
17a1694
After release cleanups.
mp911de Nov 19, 2024
d6d51be
Adopt to deprecation removals in Commons.
mp911de Nov 19, 2024
d2c45f3
Polishing.
mp911de Nov 20, 2024
5d913c8
Adopt to deprecation removals in Commons.
mp911de Nov 25, 2024
38edf0d
Prepare 4.0 M1 (2025.1.0).
christophstrobl Jan 24, 2025
0803dc4
Release version 4.0 M1 (2025.1.0).
christophstrobl Jan 24, 2025
988ad42
Prepare next development iteration.
christophstrobl Jan 24, 2025
fd3353a
After release cleanups.
christophstrobl Jan 24, 2025
3640cbe
Refine generic boundaries.
mp911de Feb 10, 2025
05270bf
Remove DbActionExecutionException.
mipo256 Dec 11, 2024
7340cdf
Polishing.
schauder Mar 17, 2025
3b124ca
Fail on annotated query methods with `@Lock`.
mipo256 Apr 1, 2025
09b15d9
Polishing.
schauder Apr 10, 2025
83650d9
574-composite-id - Prepare branch
schauder Jun 10, 2024
c9815c0
Add support for composite ids.
schauder Jun 10, 2024
5fc12fe
Improved Composite Id support.
schauder Feb 11, 2025
d99f65e
Polishing.
schauder Mar 12, 2025
effcf8c
Polishing.
mp911de Mar 19, 2025
1c0dcf1
added comments
schauder Mar 20, 2025
d29c800
fix typo
schauder Mar 20, 2025
40c21c4
removed function indirection for condition construction
schauder Mar 21, 2025
b9c7364
Immediatly construct join conditions
schauder Mar 21, 2025
f36c925
early returns
schauder Mar 21, 2025
b3ce068
remove unused code
schauder Mar 21, 2025
4b7e87b
Simplified test class names
schauder Mar 21, 2025
259bd44
renamed test entity
schauder Apr 2, 2025
059cdcd
Merged two Identifier generating methods into one.
schauder Apr 11, 2025
3a3e466
Polishing.
schauder Apr 16, 2025
07bc85d
backReferenceColumnInfo
schauder Apr 16, 2025
df19605
JavaDoc
schauder Apr 16, 2025
05eeb44
Simplified forQueryId
schauder Apr 17, 2025
b6eb3da
simplified forQueryByIds
schauder Apr 17, 2025
3b1f389
Baby Steps
schauder Apr 17, 2025
58b7a82
wip
schauder Apr 22, 2025
3077c4f
fully extracted ciBuilder.build
schauder Apr 22, 2025
faeb73f
ColumnInfosBuilder now has package scope
schauder Apr 22, 2025
4f2bbe2
moved substract method to the other methods.
schauder Apr 22, 2025
df51936
added comments explaining the Comparable interface
schauder Apr 22, 2025
ae0546b
TupleExpressions: move maybeWrap and renamed it.
schauder Apr 22, 2025
046a707
introduce and use new toColumnList method
schauder Apr 23, 2025
b9d3db2
replace tests
schauder Apr 23, 2025
d528913
partially fixing formatting
schauder Apr 23, 2025
8ae6474
remove old method
schauder Apr 23, 2025
f4cbeca
early exit for embedded
schauder Apr 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pipeline {

triggers {
pollSCM 'H/10 * * * *'
upstream(upstreamProjects: "spring-data-commons/main", threshold: hudson.model.Result.SUCCESS)
upstream(upstreamProjects: "spring-data-commons/4.0.x", threshold: hudson.model.Result.SUCCESS)
}

options {
Expand Down
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>4.0.0-1737-nullable-embedded-with-collection-574-composite-id-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data Relational Parent</name>
Expand All @@ -15,12 +15,12 @@
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>4.0.0-SNAPSHOT</version>
</parent>

<properties>
<dist.id>spring-data-jdbc</dist.id>
<springdata.commons>3.5.0-SNAPSHOT</springdata.commons>
<springdata.commons>4.0.0-SNAPSHOT</springdata.commons>
<liquibase.version>4.21.1</liquibase.version>
<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>

Expand Down
2 changes: 1 addition & 1 deletion spring-data-jdbc-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>4.0.0-1737-nullable-embedded-with-collection-574-composite-id-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
4 changes: 2 additions & 2 deletions spring-data-jdbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-data-jdbc</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>4.0.0-1737-nullable-embedded-with-collection-574-composite-id-SNAPSHOT</version>

<name>Spring Data JDBC</name>
<description>Spring Data module for JDBC repositories.</description>
Expand All @@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>4.0.0-1737-nullable-embedded-with-collection-574-composite-id-SNAPSHOT</version>
</parent>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,21 @@
*/
package org.springframework.data.jdbc.core;

import org.springframework.dao.OptimisticLockingFailureException;
import java.util.List;

import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.relational.core.conversion.AggregateChange;
import org.springframework.data.relational.core.conversion.DbAction;
import org.springframework.data.relational.core.conversion.DbActionExecutionException;
import org.springframework.data.relational.core.conversion.MutableAggregateChange;

import java.util.List;

/**
* Executes an {@link MutableAggregateChange}.
*
* @author Jens Schauder
* @author Myeonghyeon Lee
* @author Chirag Tailor
* @author Mikhail Polivakha
* @since 2.0
*/
class AggregateChangeExecutor {
Expand Down Expand Up @@ -80,42 +79,34 @@ <T> void executeDelete(AggregateChange<T> aggregateChange) {

private void execute(DbAction<?> action, JdbcAggregateChangeExecutionContext executionContext) {

try {
if (action instanceof DbAction.InsertRoot<?> insertRoot) {
executionContext.executeInsertRoot(insertRoot);
} else if (action instanceof DbAction.BatchInsertRoot<?> batchInsertRoot) {
executionContext.executeBatchInsertRoot(batchInsertRoot);
} else if (action instanceof DbAction.Insert<?> insert) {
executionContext.executeInsert(insert);
} else if (action instanceof DbAction.BatchInsert<?> batchInsert) {
executionContext.executeBatchInsert(batchInsert);
} else if (action instanceof DbAction.UpdateRoot<?> updateRoot) {
executionContext.executeUpdateRoot(updateRoot);
} else if (action instanceof DbAction.Delete<?> delete) {
executionContext.executeDelete(delete);
} else if (action instanceof DbAction.BatchDelete<?> batchDelete) {
executionContext.executeBatchDelete(batchDelete);
} else if (action instanceof DbAction.DeleteAll<?> deleteAll) {
executionContext.executeDeleteAll(deleteAll);
} else if (action instanceof DbAction.DeleteRoot<?> deleteRoot) {
executionContext.executeDeleteRoot(deleteRoot);
} else if (action instanceof DbAction.BatchDeleteRoot<?> batchDeleteRoot) {
executionContext.executeBatchDeleteRoot(batchDeleteRoot);
} else if (action instanceof DbAction.DeleteAllRoot<?> deleteAllRoot) {
executionContext.executeDeleteAllRoot(deleteAllRoot);
} else if (action instanceof DbAction.AcquireLockRoot<?> acquireLockRoot) {
executionContext.executeAcquireLock(acquireLockRoot);
} else if (action instanceof DbAction.AcquireLockAllRoot<?> acquireLockAllRoot) {
executionContext.executeAcquireLockAllRoot(acquireLockAllRoot);
} else {
throw new RuntimeException("unexpected action");
}
} catch (Exception e) {

if (e instanceof OptimisticLockingFailureException) {
throw e;
}
throw new DbActionExecutionException(action, e);
if (action instanceof DbAction.InsertRoot<?> insertRoot) {
executionContext.executeInsertRoot(insertRoot);
} else if (action instanceof DbAction.BatchInsertRoot<?> batchInsertRoot) {
executionContext.executeBatchInsertRoot(batchInsertRoot);
} else if (action instanceof DbAction.Insert<?> insert) {
executionContext.executeInsert(insert);
} else if (action instanceof DbAction.BatchInsert<?> batchInsert) {
executionContext.executeBatchInsert(batchInsert);
} else if (action instanceof DbAction.UpdateRoot<?> updateRoot) {
executionContext.executeUpdateRoot(updateRoot);
} else if (action instanceof DbAction.Delete<?> delete) {
executionContext.executeDelete(delete);
} else if (action instanceof DbAction.BatchDelete<?> batchDelete) {
executionContext.executeBatchDelete(batchDelete);
} else if (action instanceof DbAction.DeleteAll<?> deleteAll) {
executionContext.executeDeleteAll(deleteAll);
} else if (action instanceof DbAction.DeleteRoot<?> deleteRoot) {
executionContext.executeDeleteRoot(deleteRoot);
} else if (action instanceof DbAction.BatchDeleteRoot<?> batchDeleteRoot) {
executionContext.executeBatchDeleteRoot(batchDeleteRoot);
} else if (action instanceof DbAction.DeleteAllRoot<?> deleteAllRoot) {
executionContext.executeDeleteAllRoot(deleteAllRoot);
} else if (action instanceof DbAction.AcquireLockRoot<?> acquireLockRoot) {
executionContext.executeAcquireLock(acquireLockRoot);
} else if (action instanceof DbAction.AcquireLockAllRoot<?> acquireLockAllRoot) {
executionContext.executeAcquireLockAllRoot(acquireLockAllRoot);
} else {
throw new RuntimeException("unexpected action");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
Expand Down Expand Up @@ -72,19 +73,16 @@ class JdbcAggregateChangeExecutionContext {

<T> void executeInsertRoot(DbAction.InsertRoot<T> insert) {

Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Identifier.empty(),
insert.getIdValueSource());
Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Identifier.empty(), insert.getIdValueSource());
add(new DbActionExecutionResult(insert, id));
}

<T> void executeBatchInsertRoot(DbAction.BatchInsertRoot<T> batchInsertRoot) {

List<DbAction.InsertRoot<T>> inserts = batchInsertRoot.getActions();
List<InsertSubject<T>> insertSubjects = inserts.stream()
.map(insert -> InsertSubject.describedBy(insert.getEntity(), Identifier.empty())).collect(Collectors.toList());
List<InsertSubject<T>> insertSubjects = inserts.stream().map(insert -> InsertSubject.describedBy(insert.getEntity(), Identifier.empty())).collect(Collectors.toList());

Object[] ids = accessStrategy.insert(insertSubjects, batchInsertRoot.getEntityType(),
batchInsertRoot.getBatchValue());
Object[] ids = accessStrategy.insert(insertSubjects, batchInsertRoot.getEntityType(), batchInsertRoot.getBatchValue());

for (int i = 0; i < inserts.size(); i++) {
add(new DbActionExecutionResult(inserts.get(i), ids.length > 0 ? ids[i] : null));
Expand All @@ -94,17 +92,14 @@ <T> void executeBatchInsertRoot(DbAction.BatchInsertRoot<T> batchInsertRoot) {
<T> void executeInsert(DbAction.Insert<T> insert) {

Identifier parentKeys = getParentKeys(insert, converter);
Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), parentKeys,
insert.getIdValueSource());
Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), parentKeys, insert.getIdValueSource());
add(new DbActionExecutionResult(insert, id));
}

<T> void executeBatchInsert(DbAction.BatchInsert<T> batchInsert) {

List<DbAction.Insert<T>> inserts = batchInsert.getActions();
List<InsertSubject<T>> insertSubjects = inserts.stream()
.map(insert -> InsertSubject.describedBy(insert.getEntity(), getParentKeys(insert, converter)))
.collect(Collectors.toList());
List<InsertSubject<T>> insertSubjects = inserts.stream().map(insert -> InsertSubject.describedBy(insert.getEntity(), getParentKeys(insert, converter))).collect(Collectors.toList());

Object[] ids = accessStrategy.insert(insertSubjects, batchInsert.getEntityType(), batchInsert.getBatchValue());

Expand Down Expand Up @@ -176,20 +171,34 @@ private Identifier getParentKeys(DbAction.WithDependingOn<?> action, JdbcConvert
Object id = getParentId(action);

JdbcIdentifierBuilder identifier = JdbcIdentifierBuilder //
.forBackReferences(converter, context.getAggregatePath(action.getPropertyPath()), id);
.forBackReferences(converter, context.getAggregatePath(action.getPropertyPath()),
getValueProvider(id, context.getAggregatePath(action.getPropertyPath()), converter));

for (Map.Entry<PersistentPropertyPath<RelationalPersistentProperty>, Object> qualifier : action.getQualifiers()
.entrySet()) {
for (Map.Entry<PersistentPropertyPath<RelationalPersistentProperty>, Object> qualifier : action.getQualifiers().entrySet()) {
identifier = identifier.withQualifier(context.getAggregatePath(qualifier.getKey()), qualifier.getValue());
}

return identifier.build();
}

static Function<AggregatePath, Object> getValueProvider(Object idValue, AggregatePath path, JdbcConverter converter) {

RelationalPersistentEntity<?> entity = converter.getMappingContext().getPersistentEntity(path.getIdDefiningParentPath().getRequiredIdProperty().getType());

Function<AggregatePath, Object> valueProvider = ap -> {
if (entity == null) {
return idValue;
} else {
PersistentPropertyPathAccessor<Object> propertyPathAccessor = entity.getPropertyPathAccessor(idValue);
return propertyPathAccessor.getProperty(ap.getRequiredPersistentPropertyPath());
}
};
return valueProvider;
}

private Object getParentId(DbAction.WithDependingOn<?> action) {

DbAction.WithEntity<?> idOwningAction = getIdOwningAction(action,
context.getAggregatePath(action.getPropertyPath()).getIdDefiningParentPath());
DbAction.WithEntity<?> idOwningAction = getIdOwningAction(action, context.getAggregatePath(action.getPropertyPath()).getIdDefiningParentPath());

return getPotentialGeneratedIdFrom(idOwningAction);
}
Expand All @@ -198,8 +207,7 @@ private DbAction.WithEntity<?> getIdOwningAction(DbAction.WithEntity<?> action,

if (!(action instanceof DbAction.WithDependingOn<?> withDependingOn)) {

Assert.state(idPath.isRoot(),
"When the id path is not empty the id providing action should be of type WithDependingOn");
Assert.state(idPath.isRoot(), "When the id path is not empty the id providing action should be of type WithDependingOn");

return action;
}
Expand Down Expand Up @@ -267,20 +275,16 @@ <T> List<T> populateIdsIfNecessary() {

if (newEntity != action.getEntity()) {

cascadingValues.stage(insert.getDependingOn(), insert.getPropertyPath(),
qualifierValue, newEntity);
cascadingValues.stage(insert.getDependingOn(), insert.getPropertyPath(), qualifierValue, newEntity);
} else if (insert.getPropertyPath().getLeafProperty().isCollectionLike()) {

cascadingValues.gather(insert.getDependingOn(), insert.getPropertyPath(),
qualifierValue, newEntity);
cascadingValues.gather(insert.getDependingOn(), insert.getPropertyPath(), qualifierValue, newEntity);
}
}
}

if (roots.isEmpty()) {
throw new IllegalStateException(
String.format("Cannot retrieve the resulting instance(s) unless a %s or %s action was successfully executed",
DbAction.InsertRoot.class.getName(), DbAction.UpdateRoot.class.getName()));
throw new IllegalStateException(String.format("Cannot retrieve the resulting instance(s) unless a %s or %s action was successfully executed", DbAction.InsertRoot.class.getName(), DbAction.UpdateRoot.class.getName()));
}

Collections.reverse(roots);
Expand All @@ -289,23 +293,19 @@ <T> List<T> populateIdsIfNecessary() {
}

@SuppressWarnings("unchecked")
private <S> Object setIdAndCascadingProperties(DbAction.WithEntity<S> action, @Nullable Object generatedId,
StagedValues cascadingValues) {
private <S> Object setIdAndCascadingProperties(DbAction.WithEntity<S> action, @Nullable Object generatedId, StagedValues cascadingValues) {

S originalEntity = action.getEntity();

RelationalPersistentEntity<S> persistentEntity = (RelationalPersistentEntity<S>) context
.getRequiredPersistentEntity(action.getEntityType());
PersistentPropertyPathAccessor<S> propertyAccessor = converter.getPropertyAccessor(persistentEntity,
originalEntity);
RelationalPersistentEntity<S> persistentEntity = (RelationalPersistentEntity<S>) context.getRequiredPersistentEntity(action.getEntityType());
PersistentPropertyPathAccessor<S> propertyAccessor = converter.getPropertyAccessor(persistentEntity, originalEntity);

if (IdValueSource.GENERATED.equals(action.getIdValueSource())) {
propertyAccessor.setProperty(persistentEntity.getRequiredIdProperty(), generatedId);
}

// set values of changed immutables referenced by this entity
cascadingValues.forEachPath(action, (persistentPropertyPath, o) -> propertyAccessor
.setProperty(getRelativePath(action, persistentPropertyPath), o));
cascadingValues.forEachPath(action, (persistentPropertyPath, o) -> propertyAccessor.setProperty(getRelativePath(action, persistentPropertyPath), o));

return propertyAccessor.getBean();
}
Expand Down Expand Up @@ -337,8 +337,7 @@ private <T> void updateWithoutVersion(DbAction.UpdateRoot<T> update) {

if (!accessStrategy.update(update.getEntity(), update.getEntityType())) {

throw new IncorrectUpdateSemanticsDataAccessException(
String.format(UPDATE_FAILED, update.getEntity(), getIdFrom(update)));
throw new IncorrectUpdateSemanticsDataAccessException(String.format(UPDATE_FAILED, update.getEntity(), getIdFrom(update)));
}
}

Expand All @@ -359,21 +358,20 @@ private <T> void updateWithVersion(DbAction.UpdateRoot<T> update) {
*/
private static class StagedValues {

static final List<MultiValueAggregator<?>> aggregators = Arrays.asList(SetAggregator.INSTANCE, MapAggregator.INSTANCE,
ListAggregator.INSTANCE, SingleElementAggregator.INSTANCE);
static final List<MultiValueAggregator<?>> aggregators = Arrays.asList(SetAggregator.INSTANCE, MapAggregator.INSTANCE, ListAggregator.INSTANCE, SingleElementAggregator.INSTANCE);

Map<DbAction, Map<PersistentPropertyPath, StagedValue>> values = new HashMap<>();

/**
* Adds a value that needs to be set in an entity higher up in the tree of entities in the aggregate. If the
* attribute to be set is multivalued this method expects only a single element.
*
* @param action The action responsible for persisting the entity that needs the added value set. Must not be
* {@literal null}.
* @param path The path to the property in which to set the value. Must not be {@literal null}.
* @param action The action responsible for persisting the entity that needs the added value set. Must not be
* {@literal null}.
* @param path The path to the property in which to set the value. Must not be {@literal null}.
* @param qualifier If {@code path} is a qualified multivalued properties this parameter contains the qualifier. May
* be {@literal null}.
* @param value The value to be set. Must not be {@literal null}.
* be {@literal null}.
* @param value The value to be set. Must not be {@literal null}.
*/
void stage(DbAction<?> action, PersistentPropertyPath path, @Nullable Object qualifier, Object value) {

Expand All @@ -386,11 +384,9 @@ <T> StagedValue gather(DbAction<?> action, PersistentPropertyPath path, @Nullabl

MultiValueAggregator<T> aggregator = getAggregatorFor(path);

Map<PersistentPropertyPath, StagedValue> valuesForPath = this.values.computeIfAbsent(action,
dbAction -> new HashMap<>());
Map<PersistentPropertyPath, StagedValue> valuesForPath = this.values.computeIfAbsent(action, dbAction -> new HashMap<>());

StagedValue stagedValue = valuesForPath.computeIfAbsent(path,
persistentPropertyPath -> new StagedValue(aggregator.createEmptyInstance()));
StagedValue stagedValue = valuesForPath.computeIfAbsent(path, persistentPropertyPath -> new StagedValue(aggregator.createEmptyInstance()));
T currentValue = (T) stagedValue.value;

stagedValue.value = aggregator.add(currentValue, qualifier, value);
Expand Down Expand Up @@ -430,7 +426,8 @@ void forEachPath(DbAction<?> dbAction, BiConsumer<PersistentPropertyPath, Object
}

private static class StagedValue {
@Nullable Object value;
@Nullable
Object value;
boolean isStaged;

public StagedValue(@Nullable Object value) {
Expand Down
Loading