Skip to content

Commit

Permalink
Fix id setting for partial updates of collections of immutable types.
Browse files Browse the repository at this point in the history
We gather immutable entities of which the id has changed, in order to set them as values in the parent entity.
We now also gather unchanged entities.
So they get set with the changed one in the parent.

Closes #1907
Original pull request: #1920
  • Loading branch information
schauder authored and mp911de committed Oct 25, 2024
1 parent 9b9b1c1 commit 7507342
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,7 @@
*/
package org.springframework.data.jdbc.core;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -241,7 +232,7 @@ private Object getIdFrom(DbAction.WithEntity<?> idOwningAction) {
RelationalPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(idOwningAction.getEntityType());
Object identifier = persistentEntity.getIdentifierAccessor(idOwningAction.getEntity()).getIdentifier();

Assert.state(identifier != null,() -> "Couldn't obtain a required id value for " + persistentEntity);
Assert.state(identifier != null, () -> "Couldn't obtain a required id value for " + persistentEntity);

return identifier;
}
Expand All @@ -268,12 +259,22 @@ <T> List<T> populateIdsIfNecessary() {
}

// the id property was immutable, so we have to propagate changes up the tree
if (newEntity != action.getEntity() && action instanceof DbAction.Insert<?> insert) {
if (action instanceof DbAction.Insert<?> insert) {

Pair<?, ?> qualifier = insert.getQualifier();
Object qualifierValue = qualifier == null ? null : qualifier.getSecond();

cascadingValues.stage(insert.getDependingOn(), insert.getPropertyPath(),
qualifier == null ? null : qualifier.getSecond(), newEntity);
if (newEntity != action.getEntity()) {

cascadingValues.stage(insert.getDependingOn(), insert.getPropertyPath(),
qualifierValue, newEntity);

} else if (insert.getPropertyPath().getLeafProperty().isCollectionLike()) {

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

}
}
}

Expand Down Expand Up @@ -360,7 +361,7 @@ private static class StagedValues {
static final List<MultiValueAggregator> aggregators = Arrays.asList(SetAggregator.INSTANCE, MapAggregator.INSTANCE,
ListAggregator.INSTANCE, SingleElementAggregator.INSTANCE);

Map<DbAction, Map<PersistentPropertyPath, Object>> values = new HashMap<>();
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
Expand All @@ -375,18 +376,26 @@ private static class StagedValues {
*/
@SuppressWarnings("unchecked")
<T> void stage(DbAction<?> action, PersistentPropertyPath path, @Nullable Object qualifier, Object value) {
gather(action, path, qualifier, value);
values.get(action).get(path).isStaged = true;
}

<T> void gather(DbAction<?> action, PersistentPropertyPath path, @Nullable Object qualifier, Object value) {

MultiValueAggregator<T> aggregator = getAggregatorFor(path);

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

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

Object newValue = aggregator.add(currentValue, qualifier, value);

valuesForPath.put(path, newValue);
stagedValue.value = newValue;

valuesForPath.put(path, stagedValue);
}

private MultiValueAggregator getAggregatorFor(PersistentPropertyPath path) {
Expand All @@ -408,7 +417,21 @@ private MultiValueAggregator getAggregatorFor(PersistentPropertyPath path) {
* property.
*/
void forEachPath(DbAction<?> dbAction, BiConsumer<PersistentPropertyPath, Object> action) {
values.getOrDefault(dbAction, Collections.emptyMap()).forEach(action);
values.getOrDefault(dbAction, Collections.emptyMap()).forEach((persistentPropertyPath, stagedValue) -> {
if (stagedValue.isStaged) {
action.accept(persistentPropertyPath, stagedValue.value);
}
});
}

}

private static class StagedValue {
Object value;
boolean isStaged;

public StagedValue(Object value) {
this.value = value;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceCreator;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.EnabledOnFeature;
import org.springframework.data.jdbc.testing.IntegrationTest;
Expand All @@ -55,8 +56,7 @@ public class JdbcRepositoryWithListsIntegrationTests {

private static DummyEntity createDummyEntity() {

DummyEntity entity = new DummyEntity();
entity.setName("Entity Name");
DummyEntity entity = new DummyEntity(null, "Entity Name", new ArrayList<>());
return entity;
}

Expand Down Expand Up @@ -94,7 +94,7 @@ public void saveAndLoadNonEmptyList() {
assertThat(reloaded.content) //
.isNotNull() //
.extracting(e -> e.id) //
.containsExactlyInAnyOrder(element1.id, element2.id);
.containsExactlyInAnyOrder(entity.content.get(0).id, entity.content.get(1).id);
}

@Test // GH-1159
Expand Down Expand Up @@ -147,24 +147,25 @@ public void findAllLoadsList() {
@EnabledOnFeature(SUPPORTS_GENERATED_IDS_IN_REFERENCED_ENTITIES)
public void updateList() {

Element element1 = createElement("one");
Element element2 = createElement("two");
Element element3 = createElement("three");
Element element1 = new Element("one");
Element element2 = new Element("two");
Element element3 = new Element("three");

DummyEntity entity = createDummyEntity();
entity.content.add(element1);
entity.content.add(element2);

entity = repository.save(entity);

entity.content.remove(element1);
element2.content = "two changed";
entity.content.remove(0);
entity.content.set(0, new Element(entity.content.get(0).id, "two changed"));
entity.content.add(element3);

entity = repository.save(entity);

assertThat(entity.id).isNotNull();
assertThat(entity.content).allMatch(v -> v.id != null);
assertThat(entity.content).hasSize(2);

DummyEntity reloaded = repository.findById(entity.id).orElseThrow(AssertionFailedError::new);

Expand All @@ -175,8 +176,8 @@ public void updateList() {
assertThat(reloaded.content) //
.extracting(e -> e.id, e -> e.content) //
.containsExactly( //
tuple(element2.id, "two changed"), //
tuple(element3.id, "three") //
tuple(entity.content.get(0).id, "two changed"), //
tuple(entity.content.get(1).id, "three") //
);

Long count = template.queryForObject("SELECT count(1) FROM Element", new HashMap<>(), Long.class);
Expand All @@ -186,8 +187,8 @@ public void updateList() {
@Test // DATAJDBC-130
public void deletingWithList() {

Element element1 = createElement("one");
Element element2 = createElement("two");
Element element1 = new Element("one");
Element element2 = new Element("two");

DummyEntity entity = createDummyEntity();
entity.content.add(element1);
Expand All @@ -203,13 +204,6 @@ public void deletingWithList() {
assertThat(count).isEqualTo(0);
}

private Element createElement(String content) {

Element element = new Element();
element.content = content;
return element;
}

interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {}

interface RootRepository extends CrudRepository<Root, Long> {}
Expand All @@ -229,43 +223,22 @@ RootRepository rootRepository(JdbcRepositoryFactory factory) {
}
}

static class DummyEntity {
record DummyEntity(@Id Long id, String name, List<Element> content) {
}

String name;
List<Element> content = new ArrayList<>();
@Id private Long id;
record Element(@Id Long id, String content) {

public String getName() {
return this.name;
}
@PersistenceCreator
Element {}

public List<Element> getContent() {
return this.content;
Element() {
this(null, null);
}

public Long getId() {
return this.id;
Element(String content) {
this(null, content);
}

public void setName(String name) {
this.name = name;
}

public void setContent(List<Element> content) {
this.content = content;
}

public void setId(Long id) {
this.id = id;
}
}

static class Element {

String content;
@Id private Long id;

public Element() {}
}

static class Root {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
*/
package org.springframework.data.relational.core.conversion;

import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;

import org.springframework.data.mapping.PersistentPropertyPath;
Expand Down Expand Up @@ -209,7 +212,7 @@ public String toString() {
* Note that deletes for contained entities that reference the root are to be represented by separate
* {@link DbAction}s.
* </p>
*
*
* @param <T> type of the entity for which this represents a database interaction.
*/
final class DeleteRoot<T> implements DbAction<T> {
Expand Down Expand Up @@ -274,7 +277,7 @@ public String toString() {
* Note that deletes for contained entities that reference the root are to be represented by separate
* {@link DbAction}s.
* </p>
*
*
* @param <T> type of the entity for which this represents a database interaction.
*/
final class DeleteAllRoot<T> implements DbAction<T> {
Expand Down Expand Up @@ -467,7 +470,7 @@ interface WithDependingOn<T> extends WithPropertyPath<T>, WithEntity<T> {
* <p>
* Values come from parent entities but one might also add values manually.
* </p>
*
*
* @return guaranteed to be not {@code null}.
*/
Map<PersistentPropertyPath<RelationalPersistentProperty>, Object> getQualifiers();
Expand All @@ -479,15 +482,13 @@ interface WithDependingOn<T> extends WithPropertyPath<T>, WithEntity<T> {
default Pair<PersistentPropertyPath<RelationalPersistentProperty>, Object> getQualifier() {

Map<PersistentPropertyPath<RelationalPersistentProperty>, Object> qualifiers = getQualifiers();
if (qualifiers.size() == 0)
if (qualifiers.size() == 0) {
return null;

if (qualifiers.size() > 1) {
throw new IllegalStateException("Can't handle more then one qualifier");
}

Map.Entry<PersistentPropertyPath<RelationalPersistentProperty>, Object> entry = qualifiers.entrySet().iterator()
.next();
Set<Map.Entry<PersistentPropertyPath<RelationalPersistentProperty>, Object>> entries = qualifiers.entrySet();
Map.Entry<PersistentPropertyPath<RelationalPersistentProperty>, Object> entry = entries.stream().sorted(Comparator.comparing(e -> -e.getKey().getLength())).findFirst().get();

if (entry.getValue() == null) {
return null;
}
Expand Down

0 comments on commit 7507342

Please sign in to comment.