GH-2236 - Create correct collection type for RelationshipProperties.

This fixes #2236.
This commit is contained in:
Michael Simons
2021-04-19 09:54:13 +02:00
committed by Gerrit Meier
parent 75c7f12799
commit b3dfbba7ab
2 changed files with 208 additions and 29 deletions

View File

@@ -15,16 +15,16 @@
*/
package org.springframework.data.neo4j.core;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apiguardian.api.API;
import org.springframework.core.CollectionFactory;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
/**
* Internal helper class that takes care of tracking whether a related object or a collection of related objects was recreated
* due to changing immutable properties
@@ -45,19 +45,19 @@ final class RelationshipHandler {
static RelationshipHandler forProperty(Neo4jPersistentProperty property, Object rawValue) {
Cardinality cardinality;
Collection<Object> newRelationshipObjectCollection = null;
Map<Object, Object> newRelationshipObjectCollectionMap = null;
Collection<Object> newRelationshipObjectCollection = Collections.emptyList();
Map<Object, Object> newRelationshipObjectCollectionMap = Collections.emptyMap();
// Order is important here, all map based associations are dynamic, but not all dynamic associations are one to many
if (property.isCollectionLike()) {
cardinality = Cardinality.ONE_TO_MANY;
newRelationshipObjectCollection = CollectionFactory.createApproximateCollection(rawValue, ((Collection<?>) rawValue).size());
newRelationshipObjectCollection = CollectionFactory.createCollection(property.getType(), ((Collection<?>) rawValue).size());
} else if (property.isDynamicOneToManyAssociation()) {
cardinality = Cardinality.DYNAMIC_ONE_TO_MANY;
newRelationshipObjectCollectionMap = CollectionFactory.createApproximateMap(rawValue, ((Map<?, ?>) rawValue).size());
newRelationshipObjectCollectionMap = CollectionFactory.createMap(property.getType(), ((Map<?, ?>) rawValue).size());
} else if (property.isDynamicAssociation()) {
cardinality = Cardinality.DYNAMIC_ONE_TO_ONE;
newRelationshipObjectCollectionMap = CollectionFactory.createApproximateMap(rawValue, ((Map<?, ?>) rawValue).size());
newRelationshipObjectCollectionMap = CollectionFactory.createMap(property.getType(), ((Map<?, ?>) rawValue).size());
} else {
cardinality = Cardinality.ONE_TO_ONE;
}
@@ -76,9 +76,9 @@ final class RelationshipHandler {
private final Map<Object, Object> newRelatedObjectsByType;
RelationshipHandler(Neo4jPersistentProperty property,
Object rawValue, Cardinality cardinality,
Collection<Object> newRelatedObjects,
Map<Object, Object> newRelatedObjectsByType) {
Object rawValue, Cardinality cardinality,
Collection<Object> newRelatedObjects,
Map<Object, Object> newRelatedObjectsByType) {
this.property = property;
this.rawValue = rawValue;
this.cardinality = cardinality;
@@ -87,31 +87,33 @@ final class RelationshipHandler {
}
void handle(Object relatedValueToStore, Object newRelatedObject, Object potentiallyRecreatedRelatedObject) {
if (potentiallyRecreatedRelatedObject == newRelatedObject) {
return;
} else if (cardinality == Cardinality.ONE_TO_ONE) {
this.newRelatedObjects = Collections.singletonList(potentiallyRecreatedRelatedObject);
} else if (cardinality == Cardinality.ONE_TO_MANY) {
newRelatedObjects.add(potentiallyRecreatedRelatedObject);
} else {
Object key = ((Map.Entry<Object, Object>) relatedValueToStore).getKey();
if (cardinality == Cardinality.DYNAMIC_ONE_TO_ONE) {
newRelatedObjectsByType.put(key, potentiallyRecreatedRelatedObject);
if (potentiallyRecreatedRelatedObject != newRelatedObject) {
if (cardinality == Cardinality.ONE_TO_ONE) {
this.newRelatedObjects = Collections.singletonList(potentiallyRecreatedRelatedObject);
} else if (cardinality == Cardinality.ONE_TO_MANY) {
newRelatedObjects.add(potentiallyRecreatedRelatedObject);
} else {
Collection<Object> newCollection = (Collection<Object>) newRelatedObjectsByType
.computeIfAbsent(key, k -> CollectionFactory.createCollection(
property.getTypeInformation().getRequiredActualType().getType(),
((Collection) ((Map) rawValue).get(key)).size()));
newCollection.add(potentiallyRecreatedRelatedObject);
Object key = ((Map.Entry<Object, Object>) relatedValueToStore).getKey();
if (cardinality == Cardinality.DYNAMIC_ONE_TO_ONE) {
newRelatedObjectsByType.put(key, potentiallyRecreatedRelatedObject);
} else {
Collection<Object> newCollection = (Collection<Object>) newRelatedObjectsByType
.computeIfAbsent(key, k -> CollectionFactory.createCollection(
property.getTypeInformation().getRequiredActualType().getType(),
((Collection) ((Map) rawValue).get(key)).size()));
newCollection.add(potentiallyRecreatedRelatedObject);
}
}
}
}
void applyFinalResultToOwner(PersistentPropertyAccessor<?> parentPropertyAccessor) {
Object finalRelation = null;
switch (cardinality) {
case ONE_TO_ONE:
finalRelation = newRelatedObjects == null ? null : ((List) newRelatedObjects).get(0);
finalRelation = Optional.ofNullable(newRelatedObjects).flatMap(v -> v.stream().findFirst()).orElse(null);
break;
case ONE_TO_MANY:
if (!newRelatedObjects.isEmpty()) {

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2011-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.integration.imperative;
import org.junit.jupiter.api.Test;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.data.neo4j.test.Neo4jExtension;
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Michael J. Simons
*/
@Neo4jIntegrationTest
public class CollectionsIT {
private static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
private final Driver driver;
@Autowired
CollectionsIT(Driver driver) {
this.driver = driver;
}
@Test // GH-2236
void loadingOfRelPropertiesInSetsShouldWork(@Autowired Neo4jTemplate repository) {
Long id;
try (Session session = driver.session()) {
id = session.run(
"CREATE (c:CollectionChildNodeA {name: 'The Child'}) <- [:CHILDREN_WITH_PROPERTIES {prop: 'The Property'}] - (p:CollectionParentNode {name: 'The Parent'}) RETURN id(p)"
).single().get(0).asLong();
}
Optional<CollectionParentNode> optionalParent = repository.findById(id, CollectionParentNode.class);
assertThat(optionalParent).hasValueSatisfying(parent -> {
assertThat(parent.id).isNotNull();
assertThat(parent.name).isEqualTo("The Parent");
assertThat(parent.childrenWithProperties).isNotNull();
assertThat(parent.childrenWithProperties).first().satisfies(p -> {
assertThat(p.target.id).isNotNull();
assertThat(p.target.name).isEqualTo("The Child");
assertThat(p.prop).isEqualTo("The Property");
});
});
}
@Test // GH-2236
void storingOfRelPropertiesInSetsShouldWork(@Autowired Neo4jTemplate template) {
CollectionParentNode parent = new CollectionParentNode("parent");
parent.childrenWithProperties.add(new RelProperties(new CollectionChildNodeA("child"), "a property"));
parent = template.save(parent);
assertThat(parent.id).isNotNull();
assertThat(parent.childrenWithProperties).isNotNull();
assertThat(parent.childrenWithProperties).first().satisfies(p -> {
assertThat(p.target.id).isNotNull();
assertThat(p.target.name).isEqualTo("child");
assertThat(p.prop).isEqualTo("a property");
});
try (Session session = driver.session()) {
long cnt = session.run(
"MATCH (c:CollectionChildNodeA) <- [:CHILDREN_WITH_PROPERTIES] - (p:CollectionParentNode) WHERE id(p) = $id RETURN count(c) ",
Collections.singletonMap("id", parent.id)
).single().get(0).asLong();
assertThat(cnt).isEqualTo(1L);
}
}
@Node
static class CollectionParentNode {
@Id
@GeneratedValue
Long id;
final String name;
Set<RelProperties> childrenWithProperties = new HashSet<>();
CollectionParentNode(String name) {
this.name = name;
}
}
@Node
static class CollectionChildNodeA {
@Id
@GeneratedValue
Long id;
final String name;
CollectionChildNodeA(String name) {
this.name = name;
}
}
@RelationshipProperties
static class RelProperties {
@Id
@GeneratedValue
Long id;
@TargetNode
final CollectionChildNodeA target;
final String prop;
RelProperties(CollectionChildNodeA target, String prop) {
this.target = target;
this.prop = prop;
}
}
@Configuration
@EnableTransactionManagement
static class Config extends AbstractNeo4jConfig {
@Bean
public Driver driver() {
return neo4jConnectionSupport.getDriver();
}
@Override
public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException {
// Don't create repositories for the entities, otherwise they must be moved
// to a public reachable place. I didn't want that as the mapping context is polluted already
// enough with the shared package of nodes.
Neo4jMappingContext ctx = new Neo4jMappingContext(neo4JConversions);
ctx.setInitialEntitySet(new HashSet<>(Arrays.asList(CollectionParentNode.class, CollectionChildNodeA.class, RelProperties.class)));
return ctx;
}
}
}