diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 9b54d6e50..57ca3bd0d 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -883,7 +883,7 @@ public final class Neo4jTemplate implements List> relationshipPropertiesRows = new ArrayList<>(); List> newRelationshipPropertiesRows = new ArrayList<>(); List updateRelatedValuesToStore = new ArrayList<>(); - List newRelatedValuesToStore = new ArrayList<>(); + List newRelationshipPropertiesToStore = new ArrayList<>(); for (Object relatedValueToStore : relatedValuesToStore) { @@ -977,15 +977,22 @@ public final class Neo4jTemplate implements .bindAll(statementHolder.getProperties()) .run(); } - } else if (relationshipDescription.hasRelationshipProperties() && isNewRelationship && idProperty != null) { - newRelationshipPropertiesRows.add(properties); - newRelatedValuesToStore.add(relatedValueToStore); } else if (relationshipDescription.hasRelationshipProperties()) { - neo4jMappingContext.getEntityConverter().write( - ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore).getRelationshipProperties(), - properties); - - relationshipPropertiesRows.add(properties); + // check if bidi mapped already + var hlp = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore); + var hasProcessedRelationshipEntity = stateMachine.hasProcessedRelationshipEntity(propertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship()); + if (hasProcessedRelationshipEntity) { + stateMachine.requireIdUpdate(sourceEntity, relationshipDescription, canUseElementId, fromId, relatedInternalId, relationshipContext, relatedValueToStore, idProperty); + } else { + if (isNewRelationship && idProperty != null) { + newRelationshipPropertiesRows.add(properties); + newRelationshipPropertiesToStore.add(relatedValueToStore); + } else { + neo4jMappingContext.getEntityConverter().write(hlp.getRelationshipProperties(), properties); + relationshipPropertiesRows.add(properties); + } + stateMachine.storeProcessRelationshipEntity(hlp, propertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship()); + } } else { // non-dynamic relationship or relationship with properties plainRelationshipRows.add(properties); @@ -1020,9 +1027,9 @@ public final class Neo4jTemplate implements .bindAll(statementHolder.getProperties()) .run(); } - if (!newRelatedValuesToStore.isEmpty()) { + if (!newRelationshipPropertiesToStore.isEmpty()) { CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForImperativeRelationshipsWithPropertiesBatch(true, - sourceEntity, relationshipDescription, newRelatedValuesToStore, newRelationshipPropertiesRows, canUseElementId); + sourceEntity, relationshipDescription, newRelationshipPropertiesToStore, newRelationshipPropertiesRows, canUseElementId); List all = new ArrayList<>(neo4jClient.query(renderer.render(statementHolder.getStatement())) .bindAll(statementHolder.getProperties()) .fetchAs(Object.class) @@ -1031,11 +1038,14 @@ public final class Neo4jTemplate implements // assign new ids for (int i = 0; i < all.size(); i++) { Object anId = all.get(i); - assignIdToRelationshipProperties(relationshipContext, newRelatedValuesToStore.get(i), idProperty, anId); + assignIdToRelationshipProperties(relationshipContext, newRelationshipPropertiesToStore.get(i), idProperty, anId); } } } + // Possible grab missing relationship ids now for bidirectional ones, with properties, mapped in opposite directions + stateMachine.updateRelationshipIds(this::getRelationshipId); + relationshipHandler.applyFinalResultToOwner(propertyAccessor); }); @@ -1044,6 +1054,19 @@ public final class Neo4jTemplate implements return finalSubgraphRoot; } + private Optional getRelationshipId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId) { + + return neo4jClient.query(renderer.render(statement)) + .bind(convertIdValues(idProperty, fromId)) // + .to(Constants.FROM_ID_PARAMETER_NAME) // + .bind(toId) // + .to(Constants.TO_ID_PARAMETER_NAME) // + .fetchAs(Object.class) + .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) + .one(); + } + + // The pendant to {@link #saveRelatedNode(Object, NodeDescription, PropertyFilter, PropertyFilter.RelaxedPropertyPath)} // We can't do without a query, as we need to refresh the internal id private Entity loadRelatedNode(NodeDescription targetNodeDescription, Object relatedInternalId) { diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java index f9b661924..3e69ef2d9 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java @@ -418,19 +418,25 @@ public final class ReactiveNeo4jTemplate implements getProjectionFactory(), neo4jMappingContext); NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext); + Collection knownRelationshipsIds = new HashSet<>(); EntityFromDtoInstantiatingConverter converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext); return Flux.fromIterable(instances) .concatMap(instance -> { T domainObject = converter.convert(instance); @SuppressWarnings("unchecked") - Mono result = transactionalOperator.transactional(saveImpl(domainObject, pps, stateMachine) + Mono result = transactionalOperator.transactional(saveImpl(domainObject, pps, stateMachine, knownRelationshipsIds) .map(savedEntity -> (R) new DtoInstantiatingConverter(resultType, neo4jMappingContext).convertDirectly(savedEntity))); return result; }); } + private Mono saveImpl(T instance, @Nullable Collection includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) { + return saveImpl(instance, includedProperties, stateMachine, new HashSet<>()); + } + + private Mono saveImpl(T instance, @Nullable Collection includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine, Collection knownRelationshipsIds) { if (stateMachine != null && stateMachine.hasProcessedValue(instance)) { return Mono.just(instance); @@ -479,7 +485,7 @@ public final class ReactiveNeo4jTemplate implements TemplateSupport.updateVersionPropertyIfPossible(entityMetaData, propertyAccessor, newOrUpdatedNode); finalStateMachine.markEntityAsProcessed(instance, elementId); }).map(IdentitySupport::getElementId) - .flatMap(internalId -> processRelations(entityMetaData, propertyAccessor, isNewEntity, finalStateMachine, binderFunction.filter)); + .flatMap(internalId -> processRelations(entityMetaData, propertyAccessor, isNewEntity, finalStateMachine, knownRelationshipsIds, binderFunction.filter)); }); } @@ -594,7 +600,7 @@ public final class ReactiveNeo4jTemplate implements Function> binderFunction = TemplateSupport.createAndApplyPropertyFilter( pps, entityMetaData, neo4jMappingContext.getRequiredBinderFunctionFor((Class) domainClass)); - return Flux.fromIterable(entities) + return (Flux) Flux.deferContextual((ctx) -> Flux.fromIterable(entities) // Map all entities into a tuple .map(e -> Tuples.of(e, entityMetaData.isNew(e))) // Map that tuple into a tuple <, PotentiallyModified> @@ -618,11 +624,16 @@ public final class ReactiveNeo4jTemplate implements .concatMap(t -> { PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(t.getT3()); Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); - Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty)); - String internalId = idToInternalIdMapping.get(id); - return processRelations(entityMetaData, propertyAccessor, t.getT2(), new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.getT1(), internalId), + return processRelations(entityMetaData, propertyAccessor, t.getT2(), + ctx.get("stateMachine"), + ctx.get("knownRelIds"), TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData)); })) + )) + .contextWrite(ctx -> + ctx + .put("stateMachine", new NestedRelationshipProcessingStateMachine(neo4jMappingContext, null, null)) + .put("knownRelIds", new HashSet<>()) ); } @@ -878,16 +889,19 @@ public final class ReactiveNeo4jTemplate implements PersistentPropertyAccessor parentPropertyAccessor, boolean isParentObjectNew, NestedRelationshipProcessingStateMachine stateMachine, + Collection knownRelationshipsIds, PropertyFilter includeProperty ) { PropertyFilter.RelaxedPropertyPath startingPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(neo4jPersistentEntity.getUnderlyingClass()); return processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew, - stateMachine, includeProperty, startingPropertyPath); + stateMachine, knownRelationshipsIds, includeProperty, startingPropertyPath); } private Mono processNestedRelations(Neo4jPersistentEntity sourceEntity, PersistentPropertyAccessor parentPropertyAccessor, - boolean isParentObjectNew, NestedRelationshipProcessingStateMachine stateMachine, PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath previousPath) { + boolean isParentObjectNew, NestedRelationshipProcessingStateMachine stateMachine, + Collection knownRelationshipsIds, + PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath previousPath) { Object fromId = parentPropertyAccessor.getProperty(sourceEntity.getRequiredIdProperty()); List> relationshipDeleteMonos = new ArrayList<>(); @@ -932,7 +946,6 @@ public final class ReactiveNeo4jTemplate implements boolean canUseElementId = TemplateSupport.rendererRendersElementId(renderer); if (!isParentObjectNew && !stateMachine.hasProcessedRelationship(fromId, relationshipDescription)) { - List knownRelationshipsIds = new ArrayList<>(); if (idProperty != null) { for (Object relatedValueToStore : relatedValuesToStore) { if (relatedValueToStore == null) { @@ -1021,7 +1034,7 @@ public final class ReactiveNeo4jTemplate implements TemplateSupport.updateVersionPropertyIfPossible(targetEntity, targetPropertyAccessor, savedEntity); } stateMachine.markAsAliased(relatedObjectBeforeCallbacksApplied, targetPropertyAccessor.getBean()); - stateMachine.markRelationshipAsProcessed(possibleInternalLongId == null ? relatedInternalId : possibleInternalLongId, + stateMachine.markRelationshipAsProcessed(possibleInternalLongId == null ? relatedInternalId : possibleInternalLongId, relationshipDescription.getRelationshipObverse()); Object idValue = idProperty != null @@ -1037,43 +1050,63 @@ public final class ReactiveNeo4jTemplate implements properties.put(Constants.FROM_ID_PARAMETER_NAME, convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)); properties.put(Constants.TO_ID_PARAMETER_NAME, relatedInternalId); properties.put(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM, idValue); + var update = true; + if (!relationshipDescription.isDynamic() && relationshipDescription.hasRelationshipProperties()) { + var hlp = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore); + var hasProcessedRelationshipEntity = stateMachine.hasProcessedRelationshipEntity(parentPropertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship()); + if (hasProcessedRelationshipEntity) { + stateMachine.requireIdUpdate(sourceEntity, relationshipDescription, canUseElementId, fromId, relatedInternalId, relationshipContext, relatedValueToStore, idProperty); + update = false; + } else { + stateMachine.storeProcessRelationshipEntity(hlp, parentPropertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship()); + } + } List rows = new ArrayList<>(); rows.add(properties); statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, rows); // in case of no properties the bind will just return an empty map - return neo4jClient - .query(renderer.render(statementHolder.getStatement())) - .bind(convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)) // + if (update) { + return neo4jClient + .query(renderer.render(statementHolder.getStatement())) + .bind(convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)) // .to(Constants.FROM_ID_PARAMETER_NAME) // - .bind(relatedInternalId) // + .bind(relatedInternalId) // .to(Constants.TO_ID_PARAMETER_NAME) // - .bind(idValue) // - .to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) // - .bindAll(statementHolder.getProperties()) - .fetchAs(Object.class) - .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) - .one() - .flatMap(relationshipInternalId -> { - if (idProperty != null && isNewRelationship) { - relationshipContext - .getRelationshipPropertiesPropertyAccessor(relatedValueToStore) - .setProperty(idProperty, relationshipInternalId); - } + .bind(idValue) // + .to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) // + .bindAll(statementHolder.getProperties()) + .fetchAs(Object.class) + .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) + .one() + .flatMap(relationshipInternalId -> { + if (idProperty != null && isNewRelationship) { + relationshipContext + .getRelationshipPropertiesPropertyAccessor(relatedValueToStore) + .setProperty(idProperty, relationshipInternalId); + knownRelationshipsIds.add(relationshipInternalId); + } - Mono nestedRelationshipsSignal = null; - if (processState != ProcessState.PROCESSED_ALL_VALUES) { - nestedRelationshipsSignal = processNestedRelations(targetEntity, targetPropertyAccessor, targetEntity.isNew(newRelatedObject), stateMachine, includeProperty, currentPropertyPath); - } + Mono nestedRelationshipsSignal = null; + if (processState != ProcessState.PROCESSED_ALL_VALUES) { + nestedRelationshipsSignal = processNestedRelations(targetEntity, targetPropertyAccessor, targetEntity.isNew(newRelatedObject), stateMachine, knownRelationshipsIds, includeProperty, currentPropertyPath); + } - Mono getRelationshipOrRelationshipPropertiesObject = Mono.fromSupplier(() -> MappingSupport.getRelationshipOrRelationshipPropertiesObject( - neo4jMappingContext, - relationshipDescription.hasRelationshipProperties(), - relationshipProperty.isDynamicAssociation(), - relatedValueToStore, - targetPropertyAccessor)); - return nestedRelationshipsSignal == null ? getRelationshipOrRelationshipPropertiesObject : - nestedRelationshipsSignal.then(getRelationshipOrRelationshipPropertiesObject); - }); + Mono getRelationshipOrRelationshipPropertiesObject = Mono.fromSupplier(() -> MappingSupport.getRelationshipOrRelationshipPropertiesObject( + neo4jMappingContext, + relationshipDescription.hasRelationshipProperties(), + relationshipProperty.isDynamicAssociation(), + relatedValueToStore, + targetPropertyAccessor)); + return nestedRelationshipsSignal == null ? getRelationshipOrRelationshipPropertiesObject : + nestedRelationshipsSignal.then(getRelationshipOrRelationshipPropertiesObject); + }); + } + return Mono.fromSupplier(() -> MappingSupport.getRelationshipOrRelationshipPropertiesObject( + neo4jMappingContext, + relationshipDescription.hasRelationshipProperties(), + relationshipProperty.isDynamicAssociation(), + relatedValueToStore, + targetPropertyAccessor)); }) .doOnNext(potentiallyRecreatedRelatedObject -> { RelationshipHandler handler = ctx.get(CONTEXT_RELATIONSHIP_HANDLER); @@ -1095,11 +1128,24 @@ public final class ReactiveNeo4jTemplate implements .thenMany(Flux.concat(relationshipCreationCreations)) .doOnNext(objects -> objects.applyFinalResultToOwner(parentPropertyAccessor)) .checkpoint() + .then(stateMachine.updateRelationshipIds(this::getRelationshipId)) .then(Mono.fromSupplier(parentPropertyAccessor::getBean)); return deleteAndThanCreateANew; } + private Mono getRelationshipId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId) { + + return neo4jClient.query(renderer.render(statement)) + .bind(convertIdValues(idProperty, fromId)) // + .to(Constants.FROM_ID_PARAMETER_NAME) // + .bind(toId) // + .to(Constants.TO_ID_PARAMETER_NAME) // + .fetchAs(Object.class) + .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) + .one(); + } + // The pendant to {@link #saveRelatedNode(Object, Neo4jPersistentEntity, PropertyFilter, PropertyFilter.RelaxedPropertyPath)} // We can't do without a query, as we need to refresh the internal id private Mono loadRelatedNode(NodeDescription targetNodeDescription, Object relatedInternalId) { diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java index d7cc246a4..80bdbad4e 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java @@ -541,7 +541,8 @@ public enum CypherGenerator { RelationshipDescription relationship, boolean isNew, @Nullable String dynamicRelationshipType, - boolean canUseElementId) { + boolean canUseElementId, + boolean matchOnly) { Assert.isTrue(relationship.hasRelationshipProperties(), "Properties required to create a relationship with properties"); @@ -567,6 +568,12 @@ public enum CypherGenerator { .match(endNode) .where(getEndNodeIdFunction((Neo4jPersistentEntity) relationship.getTarget(), canUseElementId).apply(endNode).isEqualTo(parameter(Constants.TO_ID_PARAMETER_NAME))); + if (matchOnly) { + return startAndEndNodeMatch.match(relationshipFragment) + .returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment)) + .build(); + } + StatementBuilder.ExposesSet createOrMatch = isNew ? startAndEndNodeMatch.create(relationshipFragment) : startAndEndNodeMatch.match(relationshipFragment) diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index b46fadd0f..d4930c592 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -603,7 +603,7 @@ public final class Neo4jMappingContext extends AbstractMappingContext propMap = new HashMap<>(); // write relationship properties diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java index f8ed7a23d..dc4ab2cdf 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java @@ -24,10 +24,14 @@ import java.util.Set; import java.util.concurrent.locks.StampedLock; import org.apiguardian.api.API; +import org.neo4j.cypherdsl.core.Statement; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + /** * This stores all processed nested relations and objects during save of objects so that the recursive descent can be * stopped accordingly. @@ -66,6 +70,10 @@ public final class NestedRelationshipProcessingStateMachine { */ private final Map processedObjectsIds = new HashMap<>(); + private final Set processedRelationshipEntities = new HashSet<>(); + + private final Set requiresIdUpdate = new HashSet<>(); + public NestedRelationshipProcessingStateMachine(final Neo4jMappingContext mappingContext) { Assert.notNull(mappingContext, "Mapping context is required"); @@ -125,6 +133,31 @@ public final class NestedRelationshipProcessingStateMachine { private record RelationshipDescriptionWithSourceId(Object id, RelationshipDescription relationshipDescription) { } + private record ProcessedRelationshipEntity(MappingSupport.RelationshipPropertiesWithEntityHolder entityHolder, + Object source, Object target, RelationshipDescription relationshipDescription) { + } + + private record RelationshipIdUpdateContext(Statement cypher, Object fromId, Object toId, + NestedRelationshipContext relationshipContext, + Object relatedValueToStore, Neo4jPersistentProperty idProperty) { + } + + /** + * Supplier for arbitrary relationship ids + */ + @FunctionalInterface + public interface RelationshipIdSupplier { + Optional getId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId); + } + + /** + * Reactive Supplier for arbitrary relationship ids + */ + @FunctionalInterface + public interface ReactiveRelationshipIdSupplier { + Mono getId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId); + } + /** * Marks the passed objects as processed * @@ -228,6 +261,93 @@ public final class NestedRelationshipProcessingStateMachine { return false; } + public void storeProcessRelationshipEntity(MappingSupport.RelationshipPropertiesWithEntityHolder id, Object source, Object target, RelationshipDescription type) { + final long stamp = lock.writeLock(); + try { + this.processedRelationshipEntities.add(new ProcessedRelationshipEntity(id, source, target, type)); + } finally { + lock.unlock(stamp); + } + } + + public boolean hasProcessedRelationshipEntity(Object source, Object target, RelationshipDescription type) { + final long stamp = lock.readLock(); + try { + return this.processedRelationshipEntities.stream() + .anyMatch(r -> r.relationshipDescription().getType().equals(type.getType()) && r.relationshipDescription().getDirection().opposite() == type.getDirection() && ( + r.source() == source && r.target() == target || + r.target() == source && r.source() == target + )); + } finally { + lock.unlock(stamp); + } + } + + public void requireIdUpdate(Neo4jPersistentEntity sourceEntity, RelationshipDescription relationshipDescription, boolean canUseElementId, + Object fromId, Object toId, NestedRelationshipContext relationshipContext, Object relatedValueToStore, Neo4jPersistentProperty idProperty) { + + Statement relationshipCreationQuery = CypherGenerator.INSTANCE.prepareSaveOfRelationshipWithProperties( + sourceEntity, relationshipDescription, false, + null, canUseElementId, true); + final long stamp = lock.writeLock(); + try { + this.requiresIdUpdate.add(new RelationshipIdUpdateContext(relationshipCreationQuery, fromId, toId, relationshipContext, relatedValueToStore, idProperty)); + } finally { + lock.unlock(stamp); + } + } + + public Collection getRequiresIdUpdate() { + final long stamp = lock.readLock(); + try { + return Set.copyOf(requiresIdUpdate); + } finally { + lock.unlock(stamp); + } + } + + public void markAsUpdated(RelationshipIdUpdateContext context) { + final long stamp = lock.writeLock(); + try { + requiresIdUpdate.remove(context); + } finally { + lock.unlock(stamp); + } + } + + public void updateRelationshipIds(RelationshipIdSupplier idSupplier) { + final long stamp = lock.writeLock(); + try { + var it = requiresIdUpdate.iterator(); + while (it.hasNext()) { + var requiredIdUpdate = it.next(); + idSupplier.getId(requiredIdUpdate.cypher(), requiredIdUpdate.idProperty(), requiredIdUpdate.fromId(), requiredIdUpdate.toId()).ifPresent(anId -> { + requiredIdUpdate.relationshipContext() + .getRelationshipPropertiesPropertyAccessor(requiredIdUpdate.relatedValueToStore()) + .setProperty(requiredIdUpdate.idProperty(), anId); + it.remove(); + }); + } + } finally { + lock.unlock(stamp); + } + } + + public Mono updateRelationshipIds(ReactiveRelationshipIdSupplier idSupplier) { + return Flux.defer(() -> { + final long stamp = lock.writeLock(); + return Flux.fromIterable(requiresIdUpdate) + .flatMap(requiredIdUpdate -> Mono.just(requiredIdUpdate).zipWith(idSupplier.getId(requiredIdUpdate.cypher(), requiredIdUpdate.idProperty(), requiredIdUpdate.fromId(), requiredIdUpdate.toId()))) + .doOnNext(t -> { + var requiredIdUpdate = t.getT1(); + requiredIdUpdate.relationshipContext() + .getRelationshipPropertiesPropertyAccessor(requiredIdUpdate.relatedValueToStore()) + .setProperty(requiredIdUpdate.idProperty(), t.getT2()); + requiresIdUpdate.remove(requiredIdUpdate); + }).doOnTerminate(() -> lock.unlock(stamp)); + }).then(); + } + public void markAsAliased(Object aliasEntity, Object entityOrId) { final long stamp = lock.writeLock(); try { diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/OptimisticLockingIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/OptimisticLockingIT.java index e749af49f..883d7a0a8 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/OptimisticLockingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/OptimisticLockingIT.java @@ -399,7 +399,7 @@ class OptimisticLockingIT { try { Thread.sleep(sleep); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); } }); }); diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java index f25f4b2c5..6f782eb89 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java @@ -25,14 +25,17 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; @@ -43,6 +46,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; +import org.neo4j.cypherdsl.core.LabelExpression; import org.neo4j.cypherdsl.core.Node; import org.neo4j.cypherdsl.core.Parameter; import org.neo4j.cypherdsl.core.Property; @@ -50,7 +54,10 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.QueryRunner; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; +import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.types.Relationship; +import org.neo4j.driver.types.TypeSystem; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -151,6 +158,17 @@ import org.springframework.data.neo4j.integration.issues.gh2858.GH2858Repository import org.springframework.data.neo4j.integration.issues.gh2886.Apple; import org.springframework.data.neo4j.integration.issues.gh2886.FruitRepository; import org.springframework.data.neo4j.integration.issues.gh2886.Orange; +import org.springframework.data.neo4j.integration.issues.gh2905.BugFromV1; +import org.springframework.data.neo4j.integration.issues.gh2905.BugRelationshipV1; +import org.springframework.data.neo4j.integration.issues.gh2905.BugTargetV1; +import org.springframework.data.neo4j.integration.issues.gh2905.FromRepositoryV1; +import org.springframework.data.neo4j.integration.issues.gh2905.ToRepositoryV1; +import org.springframework.data.neo4j.integration.issues.gh2906.BugFrom; +import org.springframework.data.neo4j.integration.issues.gh2906.BugTarget; +import org.springframework.data.neo4j.integration.issues.gh2906.BugTargetContainer; +import org.springframework.data.neo4j.integration.issues.gh2906.FromRepository; +import org.springframework.data.neo4j.integration.issues.gh2906.OutgoingBugRelationship; +import org.springframework.data.neo4j.integration.issues.gh2906.ToRepository; import org.springframework.data.neo4j.integration.issues.qbe.A; import org.springframework.data.neo4j.integration.issues.qbe.ARepository; import org.springframework.data.neo4j.integration.issues.qbe.B; @@ -218,6 +236,24 @@ class IssuesIT extends TestBase { } } + // clean up known throw-away nodes / rels + @AfterEach + void cleanup(@Autowired BookmarkCapture bookmarkCapture) { + List labelsToBeRemoved = List.of("BugFromV1", "BugFrom", "BugTargetV1", "BugTarget", "BugTargetBaseV1", "BugTargetBase", "BugTargetContainer"); + var labelExpression = new LabelExpression(labelsToBeRemoved.get(0)); + for (int i = 1; i < labelsToBeRemoved.size(); i++) { + labelExpression = labelExpression.or(new LabelExpression(labelsToBeRemoved.get(i))); + } + try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { + Node nodes = Cypher.node(labelExpression); + String cypher = Cypher.match(nodes).detachDelete(nodes).build().getCypher(); + transaction.run(cypher).consume(); + transaction.commit(); + bookmarkCapture.seedWith(session.lastBookmarks()); + } + } + private static void setupGH2168(QueryRunner queryRunner) { queryRunner.run("CREATE (:DomainObject{id: 'A'})").consume(); } @@ -1217,6 +1253,310 @@ class IssuesIT extends TestBase { assertThat(fruits).allMatch(f -> f instanceof Apple || f instanceof Orange); } + @Test + @Tag("GH-2905") + void storeFromRootAggregate(@Autowired ToRepositoryV1 toRepositoryV1, @Autowired Driver driver) { + var to1 = BugTargetV1.builder().name("T1").type("BUG").build(); + + var from1 = BugFromV1.builder() + .name("F1") + .reli(BugRelationshipV1.builder().target(to1).comment("F1<-T1").build()) + .build(); + var from2 = BugFromV1.builder() + .name("F2") + .reli(BugRelationshipV1.builder().target(to1).comment("F2<-T1").build()) + .build(); + var from3 = BugFromV1.builder() + .name("F3") + .reli(BugRelationshipV1.builder().target(to1).comment("F3<-T1").build()) + .build(); + + to1.relatedBugs = Set.of(from1, from2, from3); + toRepositoryV1.save(to1); + + assertGH2905Graph(driver); + } + + @Test + @Tag("GH-2905") + void saveSingleEntities(@Autowired FromRepositoryV1 fromRepositoryV1, @Autowired ToRepositoryV1 toRepositoryV1, @Autowired Driver driver) { + var to1 = BugTargetV1.builder().name("T1").type("BUG").build(); + to1.relatedBugs = new HashSet<>(); + to1 = toRepositoryV1.save(to1); + + var from1 = BugFromV1.builder() + .name("F1") + .reli(BugRelationshipV1.builder().target(to1).comment("F1<-T1").build()) + .build(); + // This is the key to solve 2905 when you had the annotation previously, you must maintain both ends of the bidirectional relationship. + // SDN does not do this for you. + to1.relatedBugs.add(from1); + from1 = fromRepositoryV1.save(from1); + + var from2 = BugFromV1.builder() + .name("F2") + .reli(BugRelationshipV1.builder().target(to1).comment("F2<-T1").build()) + .build(); + // See above + to1.relatedBugs.add(from2); + + var from3 = BugFromV1.builder() + .name("F3") + .reli(BugRelationshipV1.builder().target(to1).comment("F3<-T1").build()) + .build(); + to1.relatedBugs.add(from3); + + // See above + fromRepositoryV1.saveAll(List.of(from1, from2, from3)); + + assertGH2905Graph(driver); + } + + private static void assertGH2905Graph(Driver driver) { + var result = driver.executableQuery("MATCH (t:BugTargetV1) -[:RELI] ->(f:BugFromV1) RETURN t, collect(f) AS f").execute().records(); + assertThat(result) + .hasSize(1) + .element(0).satisfies(r -> { + assertThat(r.get("t")).matches(TypeSystem.getDefault().NODE()::isTypeOf); + assertThat(r.get("f")) + .matches(TypeSystem.getDefault().LIST()::isTypeOf) + .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) + .hasSize(3); + }); + } + + @Test + @Tag("GH-2906") + void storeFromRootAggregateToLeaf(@Autowired ToRepository toRepository, @Autowired Driver driver) { + var to1 = new BugTarget("T1", "BUG"); + + var from1 = new BugFrom("F1", "F1<-T1", to1); + var from2 = new BugFrom("F2", "F2<-T1", to1); + var from3 = new BugFrom("F3", "F3<-T1", to1); + + to1.relatedBugs = Set.of( + new OutgoingBugRelationship(from1.reli.comment, from1), + new OutgoingBugRelationship(from2.reli.comment, from2), + new OutgoingBugRelationship(from3.reli.comment, from3) + ); + toRepository.save(to1); + + assertGH2906Graph(driver); + } + + + @Test + @Tag("GH-2906") + void storeFromRootAggregateToContainer(@Autowired ToRepository toRepository, @Autowired Driver driver) { + + var t1 = new BugTarget("T1", "BUG"); + var t2 = new BugTarget("T2", "BUG"); + + var to1 = new BugTargetContainer("C1"); + to1.items.add(t1); + to1.items.add(t2); + + var from1 = new BugFrom("F1", "F1<-T1", to1); + var from2 = new BugFrom("F2", "F2<-T1", to1); + var from3 = new BugFrom("F3", "F3<-T1", to1); + + to1.relatedBugs = Set.of( + new OutgoingBugRelationship(from1.reli.comment, from1), + new OutgoingBugRelationship(from2.reli.comment, from2), + new OutgoingBugRelationship(from3.reli.comment, from3) + ); + toRepository.save(to1); + + assertGH2906Graph(driver); + } + + @Test + @Tag("GH-2906") + void saveSingleEntitiesToLeaf(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, @Autowired Driver driver) { + + var to1 = new BugTarget("T1", "BUG"); + to1 = toRepository.save(to1); + + var from1 = new BugFrom("F1", "F1<-T1", to1); + to1.relatedBugs.add(new OutgoingBugRelationship(from1.reli.comment, from1)); + from1 = fromRepository.save(from1); + + assertThat(from1.reli.id).isNotNull(); + assertThat(from1.reli.target.relatedBugs).first().extracting(r -> r.id).isNotNull(); + + var from2 = new BugFrom("F2", "F2<-T1", to1); + to1.relatedBugs.add(new OutgoingBugRelationship(from2.reli.comment, from2)); + + var from3 = new BugFrom("F3", "F3<-T1", to1); + to1.relatedBugs.add(new OutgoingBugRelationship(from3.reli.comment, from3)); + + // See above + var bugs = fromRepository.saveAll(List.of(from1, from2, from3)); + for (BugFrom from : bugs) { + assertThat(from.reli.id).isNotNull(); + assertThat(from.reli.target.relatedBugs).first().extracting(r -> r.id).isNotNull(); + } + + assertGH2906Graph(driver); + + var from1Loaded = fromRepository.findById(from1.uuid).orElseThrow(); + assertThat(from1Loaded.reli).isNotNull(); + } + + @Test + @Tag("GH-2906") + void saveSingleEntitiesToContainer(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, @Autowired Driver driver) { + + var t1 = new BugTarget("T1", "BUG"); + var t2 = new BugTarget("T2", "BUG"); + + var to1 = new BugTargetContainer("C1"); + to1.items.add(t1); + to1.items.add(t2); + + to1 = toRepository.save(to1); + + var from1 = new BugFrom("F1", "F1<-T1", to1); + to1.relatedBugs.add(new OutgoingBugRelationship(from1.reli.comment, from1)); + + var from2 = new BugFrom("F2", "F2<-T1", to1); + to1.relatedBugs.add(new OutgoingBugRelationship(from2.reli.comment, from2)); + + var from3 = new BugFrom("F3", "F3<-T1", to1); + to1.relatedBugs.add(new OutgoingBugRelationship(from3.reli.comment, from3)); + + // See above + fromRepository.saveAll(List.of(from1, from2, from3)); + + assertGH2906Graph(driver); + } + + @Test + @Tag("GH-2906") + void saveSingleEntitiesViaServiceToContainer(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, @Autowired Driver driver) { + + var t1 = new BugTarget("T1", "BUG"); + var t2 = new BugTarget("T2", "BUG"); + + var to1 = new BugTargetContainer("C1"); + to1.items.add(t1); + to1.items.add(t2); + + to1 = toRepository.save(to1); + var uuid = to1.uuid; + to1 = null; + + var from1 = new BugFrom("F1", "F1<-T1", null); + from1 = saveGH2906Entity(from1, uuid, fromRepository, toRepository); + + var from2 = new BugFrom("F2", "F2<-T1", null); + from2 = saveGH2906Entity(from2, uuid, fromRepository, toRepository); + + var from3 = new BugFrom("F3", "F3<-T1", null); + from3 = saveGH2906Entity(from3, uuid, fromRepository, toRepository); + + assertGH2906Graph(driver); + } + + @Test + @Tag("GH-2906") + void saveTwoSingleEntitiesViaServiceToContainer(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, @Autowired Driver driver) { + + var t1 = new BugTarget("T1", "BUG"); + var t2 = new BugTarget("T2", "BUG"); + + var to1 = new BugTargetContainer("C1"); + to1.items.add(t1); + to1.items.add(t2); + + to1 = toRepository.save(to1); + var uuid = to1.uuid; + + var from1 = new BugFrom("F1", "F1<-T1", null); + saveGH2906Entity(from1, uuid, fromRepository, toRepository); + + var from2 = new BugFrom("F2", "F2<-T1", null); + saveGH2906Entity(from2, uuid, fromRepository, toRepository); + + assertGH2906Graph(driver, 2); + } + + @Test + @Tag("GH-2906") + void saveSingleEntitiesViaServiceToLeaf(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, @Autowired Driver driver) { + + var uuid = toRepository.save(new BugTarget("T1", "BUG")).uuid; + + var e1 = saveGH2906Entity(new BugFrom("F1", "F1<-T1", null), uuid, fromRepository, toRepository); + + assertThat(e1.reli.id).isNotNull(); + assertThat(e1.reli.target.relatedBugs).first().extracting(r -> r.id).isNotNull(); + + e1 = saveGH2906Entity(new BugFrom("F2", "F2<-T1", null), uuid, fromRepository, toRepository); + assertThat(e1.reli.id).isNotNull(); + assertThat(e1.reli.target.relatedBugs).first().extracting(r -> r.id).isNotNull(); + + e1 = saveGH2906Entity(new BugFrom("F3", "F3<-T1", null), uuid, fromRepository, toRepository); + assertThat(e1.reli.id).isNotNull(); + assertThat(e1.reli.target.relatedBugs).first().extracting(r -> r.id).isNotNull(); + + assertGH2906Graph(driver); + } + + @Test + @Tag("GH-2906") + void saveTwoSingleEntitiesViaServiceToLeaf(@Autowired FromRepository fromRepository, @Autowired ToRepository toRepository, @Autowired Driver driver) { + + var to1 = new BugTarget("T1", "BUG"); + to1 = toRepository.save(to1); + var uuid = to1.uuid; + to1 = null; + + var from1 = new BugFrom("F1", "F1<-T1", null); + from1 = saveGH2906Entity(from1, uuid, fromRepository, toRepository); + + var from2 = new BugFrom("F2", "F2<-T1", null); + from2 = saveGH2906Entity(from2, uuid, fromRepository, toRepository); + + assertGH2906Graph(driver, 2); + } + + private BugFrom saveGH2906Entity(BugFrom from, String uuid, FromRepository fromRepository, ToRepository toRepository) { + var to = toRepository.findById(uuid).orElseThrow(); + + from.reli.target = to; + to.relatedBugs.add(new OutgoingBugRelationship(from.reli.comment, from)); + + return fromRepository.save(from); + } + + private static void assertGH2906Graph(Driver driver) { + assertGH2906Graph(driver, 3); + } + + private static void assertGH2906Graph(Driver driver, int cnt) { + + var expectedNodes = IntStream.rangeClosed(1, cnt).mapToObj(i -> String.format("F%d", i)).toArray(String[]::new); + var expectedRelationships = IntStream.rangeClosed(1, cnt).mapToObj(i -> String.format("F%d<-T1", i)).toArray(String[]::new); + + var result = driver.executableQuery("MATCH (t:BugTargetBase) -[r:RELI] ->(f:BugFrom) RETURN t, collect(f) AS f, collect(r) AS r").execute().records(); + assertThat(result) + .hasSize(1) + .element(0).satisfies(r -> { + assertThat(r.get("t")).matches(TypeSystem.getDefault().NODE()::isTypeOf); + assertThat(r.get("f")) + .matches(TypeSystem.getDefault().LIST()::isTypeOf) + .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) + .map(node -> ((org.neo4j.driver.types.Node) node).get("name").asString()) + .containsExactlyInAnyOrder(expectedNodes); + assertThat(r.get("r")) + .matches(TypeSystem.getDefault().LIST()::isTypeOf) + .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) + .map(rel -> ((Relationship) rel).get("comment").asString()) + .containsExactlyInAnyOrder(expectedRelationships); + }); + } + @Configuration @EnableTransactionManagement @EnableNeo4jRepositories(namedQueriesLocation = "more-custom-queries.properties") diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/ReactiveIssuesIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/ReactiveIssuesIT.java index be0266366..51909bf63 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/ReactiveIssuesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/ReactiveIssuesIT.java @@ -15,9 +15,28 @@ */ package org.springframework.data.neo4j.integration.issues; +import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; +import org.neo4j.cypherdsl.core.Cypher; +import org.neo4j.cypherdsl.core.LabelExpression; +import org.neo4j.cypherdsl.core.Node; +import org.neo4j.driver.Value; +import org.neo4j.driver.types.Relationship; +import org.neo4j.driver.types.TypeSystem; +import org.springframework.data.neo4j.integration.issues.gh2905.BugFromV1; +import org.springframework.data.neo4j.integration.issues.gh2905.BugRelationshipV1; +import org.springframework.data.neo4j.integration.issues.gh2905.BugTargetV1; +import org.springframework.data.neo4j.integration.issues.gh2905.ReactiveFromRepositoryV1; +import org.springframework.data.neo4j.integration.issues.gh2905.ReactiveToRepositoryV1; +import org.springframework.data.neo4j.integration.issues.gh2906.BugFrom; +import org.springframework.data.neo4j.integration.issues.gh2906.BugTarget; +import org.springframework.data.neo4j.integration.issues.gh2906.BugTargetContainer; +import org.springframework.data.neo4j.integration.issues.gh2906.OutgoingBugRelationship; +import org.springframework.data.neo4j.integration.issues.gh2906.ReactiveFromRepository; +import org.springframework.data.neo4j.integration.issues.gh2906.ReactiveToRepository; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -26,10 +45,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayNameGeneration; @@ -98,9 +121,17 @@ class ReactiveIssuesIT extends TestBase { @BeforeEach void setup(@Autowired BookmarkCapture bookmarkCapture) { + List labelsToBeRemoved = List.of("BugFromV1", "BugFrom", "BugTargetV1", "BugTarget", "BugTargetBaseV1", "BugTargetBase", "BugTargetContainer"); + var labelExpression = new LabelExpression(labelsToBeRemoved.get(0)); + for (int i = 1; i < labelsToBeRemoved.size(); i++) { + labelExpression = labelExpression.or(new LabelExpression(labelsToBeRemoved.get(i))); + } try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { try (Transaction transaction = session.beginTransaction()) { setupGH2289(transaction); + Node nodes = Cypher.node(labelExpression); + String cypher = Cypher.match(nodes).detachDelete(nodes).build().getCypher(); + transaction.run(cypher).consume(); transaction.commit(); } bookmarkCapture.seedWith(session.lastBookmarks()); @@ -425,6 +456,315 @@ class ReactiveIssuesIT extends TestBase { .verifyComplete(); } + @Test + @Tag("GH-2905") + void storeFromRootAggregate(@Autowired ReactiveToRepositoryV1 toRepositoryV1, @Autowired Driver driver) { + var to1 = BugTargetV1.builder().name("T1").type("BUG").build(); + + var from1 = BugFromV1.builder() + .name("F1") + .reli(BugRelationshipV1.builder().target(to1).comment("F1<-T1").build()) + .build(); + var from2 = BugFromV1.builder() + .name("F2") + .reli(BugRelationshipV1.builder().target(to1).comment("F2<-T1").build()) + .build(); + var from3 = BugFromV1.builder() + .name("F3") + .reli(BugRelationshipV1.builder().target(to1).comment("F3<-T1").build()) + .build(); + + to1.relatedBugs = Set.of(from1, from2, from3); + toRepositoryV1.save(to1).then().as(StepVerifier::create).expectComplete().verify(); + + assertGH2905Graph(driver); + } + + @Test + @Tag("GH-2905") + void saveSingleEntities(@Autowired ReactiveFromRepositoryV1 fromRepositoryV1, @Autowired ReactiveToRepositoryV1 toRepositoryV1, @Autowired Driver driver) { + var bugTargetV1 = BugTargetV1.builder().name("T1").type("BUG").build(); + bugTargetV1.relatedBugs = new HashSet<>(); + toRepositoryV1.save(bugTargetV1).flatMapMany(to1 -> { + + var from1 = BugFromV1.builder() + .name("F1") + .reli(BugRelationshipV1.builder().target(to1).comment("F1<-T1").build()) + .build(); + // This is the key to solve 2905 when you had the annotation previously, you must maintain both ends of the bidirectional relationship. + // SDN does not do this for you. + to1.relatedBugs.add(from1); + + var from2 = BugFromV1.builder() + .name("F2") + .reli(BugRelationshipV1.builder().target(to1).comment("F2<-T1").build()) + .build(); + // See above + to1.relatedBugs.add(from2); + + var from3 = BugFromV1.builder() + .name("F3") + .reli(BugRelationshipV1.builder().target(to1).comment("F3<-T1").build()) + .build(); + to1.relatedBugs.add(from3); + + // See above + return fromRepositoryV1.saveAll(List.of(from1, from2, from3)); + }).then().as(StepVerifier::create).expectComplete().verify(); + + assertGH2905Graph(driver); + } + + private static void assertGH2905Graph(Driver driver) { + var result = driver.executableQuery("MATCH (t:BugTargetV1) -[:RELI] ->(f:BugFromV1) RETURN t, collect(f) AS f").execute().records(); + assertThat(result) + .hasSize(1) + .element(0).satisfies(r -> { + assertThat(r.get("t")).matches(TypeSystem.getDefault().NODE()::isTypeOf); + assertThat(r.get("f")) + .matches(TypeSystem.getDefault().LIST()::isTypeOf) + .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) + .hasSize(3); + }); + } + + @Test + @Tag("GH-2906") + void storeFromRootAggregateToLeaf(@Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + var to1 = new BugTarget("T1", "BUG"); + + var from1 = new BugFrom("F1", "F1<-T1", to1); + var from2 = new BugFrom("F2", "F2<-T1", to1); + var from3 = new BugFrom("F3", "F3<-T1", to1); + + to1.relatedBugs = Set.of( + new OutgoingBugRelationship(from1.reli.comment, from1), + new OutgoingBugRelationship(from2.reli.comment, from2), + new OutgoingBugRelationship(from3.reli.comment, from3) + ); + toRepository.save(to1).as(StepVerifier::create).expectNextCount(1).verifyComplete(); + + assertGH2906Graph(driver); + } + + + @Test + @Tag("GH-2906") + void storeFromRootAggregateToContainer(@Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + + var t1 = new BugTarget("T1", "BUG"); + var t2 = new BugTarget("T2", "BUG"); + + var to1 = new BugTargetContainer("C1"); + to1.items.add(t1); + to1.items.add(t2); + + var from1 = new BugFrom("F1", "F1<-T1", to1); + var from2 = new BugFrom("F2", "F2<-T1", to1); + var from3 = new BugFrom("F3", "F3<-T1", to1); + + to1.relatedBugs = Set.of( + new OutgoingBugRelationship(from1.reli.comment, from1), + new OutgoingBugRelationship(from2.reli.comment, from2), + new OutgoingBugRelationship(from3.reli.comment, from3) + ); + toRepository.save(to1).as(StepVerifier::create).expectNextCount(1).verifyComplete(); + + assertGH2906Graph(driver); + } + + @Test + @Tag("GH-2906") + void saveSingleEntitiesToLeaf(@Autowired ReactiveFromRepository fromRepository, @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + + var bt = new BugTarget("T1", "BUG"); + toRepository.save(bt).flatMapMany(to1 -> { + + var from1 = new BugFrom("F1", "F1<-T1", to1); + to1.relatedBugs.add(new OutgoingBugRelationship(from1.reli.comment, from1)); + + var from2 = new BugFrom("F2", "F2<-T1", to1); + to1.relatedBugs.add(new OutgoingBugRelationship(from2.reli.comment, from2)); + + var from3 = new BugFrom("F3", "F3<-T1", to1); + to1.relatedBugs.add(new OutgoingBugRelationship(from3.reli.comment, from3)); + + return fromRepository.saveAll(List.of(from1, from2, from3)).collectList().doOnNext(bugs -> { + for (BugFrom from : bugs) { + assertThat(from.reli.id).isNotNull(); + assertThat(from.reli.target.relatedBugs).first().extracting(r -> r.id).isNotNull(); + } + }); + }).then().as(StepVerifier::create).expectComplete().verify(); + + assertGH2906Graph(driver); + } + + @Test + @Tag("GH-2906") + void saveSingleEntitiesToContainer(@Autowired ReactiveFromRepository fromRepository, @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + + var t1 = new BugTarget("T1", "BUG"); + var t2 = new BugTarget("T2", "BUG"); + + var to1 = new BugTargetContainer("C1"); + to1.items.add(t1); + to1.items.add(t2); + + to1 = toRepository.save(to1).block(); + + var from1 = new BugFrom("F1", "F1<-T1", to1); + to1.relatedBugs.add(new OutgoingBugRelationship(from1.reli.comment, from1)); + + var from2 = new BugFrom("F2", "F2<-T1", to1); + to1.relatedBugs.add(new OutgoingBugRelationship(from2.reli.comment, from2)); + + var from3 = new BugFrom("F3", "F3<-T1", to1); + to1.relatedBugs.add(new OutgoingBugRelationship(from3.reli.comment, from3)); + + // See above + fromRepository.saveAll(List.of(from1, from2, from3)).collectList().block(); + + assertGH2906Graph(driver); + } + + @Test + @Tag("GH-2906") + void saveSingleEntitiesViaServiceToContainer(@Autowired ReactiveFromRepository fromRepository, @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + + var t1 = new BugTarget("T1", "BUG"); + var t2 = new BugTarget("T2", "BUG"); + + var to1 = new BugTargetContainer("C1"); + to1.items.add(t1); + to1.items.add(t2); + + toRepository.save(to1) + .flatMapMany(x -> { + var uuid = x.uuid; + var from1 = new BugFrom("F1", "F1<-T1", null); + var from2 = new BugFrom("F2", "F2<-T1", null); + var from3 = new BugFrom("F3", "F3<-T1", null); + + return Flux.concat(saveGH2906Entity(from1, uuid, fromRepository, toRepository), saveGH2906Entity(from2, uuid, fromRepository, toRepository), saveGH2906Entity(from3, uuid, fromRepository, toRepository)); + }) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); + + assertGH2906Graph(driver); + } + + @Test + @Tag("GH-2906") + void saveTwoSingleEntitiesViaServiceToContainer(@Autowired ReactiveFromRepository fromRepository, @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + + var t1 = new BugTarget("T1", "BUG"); + var t2 = new BugTarget("T2", "BUG"); + + var to1 = new BugTargetContainer("C1"); + to1.items.add(t1); + to1.items.add(t2); + + toRepository.save(to1).flatMapMany(x -> { + var uuid = x.uuid; + var from1 = new BugFrom("F1", "F1<-T1", null); + var from2 = new BugFrom("F2", "F2<-T1", null); + + return Flux.concat(saveGH2906Entity(from1, uuid, fromRepository, toRepository), saveGH2906Entity(from2, uuid, fromRepository, toRepository)); + }) + .then() + .as(StepVerifier::create) + .expectComplete() + .verify(); + + assertGH2906Graph(driver, 2); + } + + @Test + @Tag("GH-2906") + void saveSingleEntitiesViaServiceToLeaf(@Autowired ReactiveFromRepository fromRepository, @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + + toRepository.save(new BugTarget("T1", "BUG")) + .map(x -> x.uuid) + .flatMapMany(uuid -> Flux.concat( + saveGH2906Entity(new BugFrom("F1", "F1<-T1", null), uuid, fromRepository, toRepository) + .doOnNext(assertRelations()), + saveGH2906Entity(new BugFrom("F2", "F2<-T1", null), uuid, fromRepository, toRepository) + .doOnNext(assertRelations()), + saveGH2906Entity(new BugFrom("F3", "F3<-T1", null), uuid, fromRepository, toRepository) + .doOnNext(assertRelations()) + + )).then() + .as(StepVerifier::create) + .expectComplete().verify(); + + assertGH2906Graph(driver); + } + + private static Consumer assertRelations() { + return e1 -> { + assertThat(e1.reli.id).isNotNull(); + assertThat(e1.reli.target.relatedBugs).first().extracting(r -> r.id).isNotNull(); + }; + } + + @Test + @Tag("GH-2906") + void saveTwoSingleEntitiesViaServiceToLeaf(@Autowired ReactiveFromRepository fromRepository, @Autowired ReactiveToRepository toRepository, @Autowired Driver driver) { + + var to1 = new BugTarget("T1", "BUG"); + toRepository.save(to1) + .map(x -> x.uuid) + .flatMapMany(uuid -> Flux.concat( + saveGH2906Entity(new BugFrom("F1", "F1<-T1", null), uuid, fromRepository, toRepository), + saveGH2906Entity(new BugFrom("F2", "F2<-T1", null), uuid, fromRepository, toRepository) + + )).then() + .as(StepVerifier::create) + .expectComplete().verify(); + + assertGH2906Graph(driver, 2); + } + + private Mono saveGH2906Entity(BugFrom from, String uuid, ReactiveFromRepository fromRepository, ReactiveToRepository toRepository) { + return toRepository.findById(uuid).flatMap(to -> { + + from.reli.target = to; + to.relatedBugs.add(new OutgoingBugRelationship(from.reli.comment, from)); + + return fromRepository.save(from); + }); + } + + private static void assertGH2906Graph(Driver driver) { + assertGH2906Graph(driver, 3); + } + + private static void assertGH2906Graph(Driver driver, int cnt) { + + var expectedNodes = IntStream.rangeClosed(1, cnt).mapToObj(i -> String.format("F%d", i)).toArray(String[]::new); + var expectedRelationships = IntStream.rangeClosed(1, cnt).mapToObj(i -> String.format("F%d<-T1", i)).toArray(String[]::new); + + var result = driver.executableQuery("MATCH (t:BugTargetBase) -[r:RELI] ->(f:BugFrom) RETURN t, collect(f) AS f, collect(r) AS r").execute().records(); + assertThat(result) + .hasSize(1) + .element(0).satisfies(r -> { + assertThat(r.get("t")).matches(TypeSystem.getDefault().NODE()::isTypeOf); + assertThat(r.get("f")) + .matches(TypeSystem.getDefault().LIST()::isTypeOf) + .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) + .map(node -> ((org.neo4j.driver.types.Node) node).get("name").asString()) + .containsExactlyInAnyOrder(expectedNodes); + assertThat(r.get("r")) + .matches(TypeSystem.getDefault().LIST()::isTypeOf) + .extracting(Value::asList, as(InstanceOfAssertFactories.LIST)) + .map(rel -> ((Relationship) rel).get("comment").asString()) + .containsExactlyInAnyOrder(expectedRelationships); + }); + } + @Configuration @EnableTransactionManagement @EnableReactiveNeo4jRepositories(namedQueriesLocation = "more-custom-queries.properties") diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugFromV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugFromV1.java new file mode 100644 index 000000000..088181374 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugFromV1.java @@ -0,0 +1,85 @@ +/* + * Copyright 2011-2024 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.issues.gh2905; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Relationship; +import org.springframework.data.neo4j.core.support.UUIDStringGenerator; + +/** + * @author Mathias Kühn + */ +@SuppressWarnings("HiddenField") // Not worth cleaning up the Delomboked version +public class BugFromV1 { + @Id + @GeneratedValue(UUIDStringGenerator.class) + protected String uuid; + + private String name; + + @Relationship(type = "RELI", direction = Relationship.Direction.INCOMING) + private BugRelationshipV1 reli; + + BugFromV1(String uuid, String name, BugRelationshipV1 reli) { + this.uuid = uuid; + this.name = name; + this.reli = reli; + } + + /** + * Lombok builder + */ + public static BugFromBuilder builder() { + return new BugFromBuilder(); + } + + /** + * Lombok builder + */ + public static class BugFromBuilder { + private String uuid; + private String name; + private BugRelationshipV1 reli; + + BugFromBuilder() { + } + + public BugFromBuilder uuid(String uuid) { + this.uuid = uuid; + return this; + } + + public BugFromBuilder name(String name) { + this.name = name; + return this; + } + + public BugFromBuilder reli(BugRelationshipV1 reli) { + this.reli = reli; + return this; + } + + public BugFromV1 build() { + return new BugFromV1(this.uuid, this.name, this.reli); + } + + @Override + public String toString() { + return "BugFrom.BugFromBuilder(uuid=" + this.uuid + ", name=" + this.name + ", reli=" + this.reli + ")"; + } + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugRelationshipV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugRelationshipV1.java new file mode 100644 index 000000000..9933f9342 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugRelationshipV1.java @@ -0,0 +1,80 @@ +/* + * Copyright 2011-2024 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.issues.gh2905; + +import org.springframework.data.neo4j.core.schema.RelationshipId; +import org.springframework.data.neo4j.core.schema.RelationshipProperties; +import org.springframework.data.neo4j.core.schema.TargetNode; + +/** + * @author Mathias Kühn + */ +@SuppressWarnings("HiddenField") // Not worth cleaning up the Delomboked version +@RelationshipProperties +public class BugRelationshipV1 { + @RelationshipId + protected Long id; + + protected String comment; + + @TargetNode + private BugTargetBaseV1 target; + + BugRelationshipV1(Long id, String comment, BugTargetBaseV1 target) { + this.id = id; + this.comment = comment; + this.target = target; + } + + public static BugRelationshipBuilder builder() { + return new BugRelationshipBuilder(); + } + + /** + * Lombok builder + */ + public static class BugRelationshipBuilder { + private Long id; + private String comment; + private BugTargetBaseV1 target; + + BugRelationshipBuilder() { + } + + public BugRelationshipBuilder id(Long id) { + this.id = id; + return this; + } + + public BugRelationshipBuilder comment(String comment) { + this.comment = comment; + return this; + } + + public BugRelationshipBuilder target(BugTargetBaseV1 target) { + this.target = target; + return this; + } + + public BugRelationshipV1 build() { + return new BugRelationshipV1(this.id, this.comment, this.target); + } + + public String toString() { + return "BugRelationship.BugRelationshipBuilder(id=" + this.id + ", comment=" + this.comment + ", target=" + this.target + ")"; + } + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetBaseV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetBaseV1.java new file mode 100644 index 000000000..f252f2555 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetBaseV1.java @@ -0,0 +1,45 @@ +/* + * Copyright 2011-2024 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.issues.gh2905; + +import java.util.Set; + +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.Relationship; +import org.springframework.data.neo4j.core.support.UUIDStringGenerator; + +/** + * @author Mathias Kühn + */ +@Node +public abstract class BugTargetBaseV1 { + @Id + @GeneratedValue(UUIDStringGenerator.class) + protected String uuid; + + private String name; + + @Relationship(type = "RELI", direction = Relationship.Direction.OUTGOING) + public Set relatedBugs; + + BugTargetBaseV1(String uuid, String name, Set relatedBugs) { + this.uuid = uuid; + this.name = name; + this.relatedBugs = relatedBugs; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetV1.java new file mode 100644 index 000000000..4ada3c870 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/BugTargetV1.java @@ -0,0 +1,76 @@ +/* + * Copyright 2011-2024 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.issues.gh2905; + +import java.util.Set; + +/** + * @author Mathias Kühn + */ +@SuppressWarnings("HiddenField") // Not worth cleaning up the Delomboked version +public class BugTargetV1 extends BugTargetBaseV1 { + private String type; + + BugTargetV1(String uuid, String name, Set relatedBugs, String type) { + super(uuid, name, relatedBugs); + this.type = type; + } + + public static BugTargetBuilder builder() { + return new BugTargetBuilder(); + } + + /** + * Builder + */ + public static class BugTargetBuilder { + private String uuid; + private String name; + private Set relatedBugs; + private String type; + + BugTargetBuilder() { + } + + public BugTargetBuilder uuid(String uuid) { + this.uuid = uuid; + return this; + } + + public BugTargetBuilder name(String name) { + this.name = name; + return this; + } + + public BugTargetBuilder relatedBugs(Set relatedBugs) { + this.relatedBugs = relatedBugs; + return this; + } + + public BugTargetBuilder type(String type) { + this.type = type; + return this; + } + + public BugTargetV1 build() { + return new BugTargetV1(this.uuid, this.name, this.relatedBugs, this.type); + } + + public String toString() { + return "BugTarget.BugTargetBuilder(uuid=" + this.uuid + ", name=" + this.name + ", relatedBugs=" + this.relatedBugs + ", type=" + this.type + ")"; + } + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/FromRepositoryV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/FromRepositoryV1.java new file mode 100644 index 000000000..256882356 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/FromRepositoryV1.java @@ -0,0 +1,24 @@ +/* + * Copyright 2011-2024 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.issues.gh2905; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +/** + * @author Gerrit Meier + */ +public interface FromRepositoryV1 extends Neo4jRepository { +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveFromRepositoryV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveFromRepositoryV1.java new file mode 100644 index 000000000..09e7ff7cd --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveFromRepositoryV1.java @@ -0,0 +1,24 @@ +/* + * Copyright 2011-2024 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.issues.gh2905; + +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; + +/** + * @author Gerrit Meier + */ +public interface ReactiveFromRepositoryV1 extends ReactiveNeo4jRepository { +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveToRepositoryV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveToRepositoryV1.java new file mode 100644 index 000000000..763f48820 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ReactiveToRepositoryV1.java @@ -0,0 +1,24 @@ +/* + * Copyright 2011-2024 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.issues.gh2905; + +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; + +/** + * @author Gerrit Meier + */ +public interface ReactiveToRepositoryV1 extends ReactiveNeo4jRepository { +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ToRepositoryV1.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ToRepositoryV1.java new file mode 100644 index 000000000..6400eefde --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2905/ToRepositoryV1.java @@ -0,0 +1,24 @@ +/* + * Copyright 2011-2024 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.issues.gh2905; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +/** + * @author Gerrit Meier + */ +public interface ToRepositoryV1 extends Neo4jRepository { +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugFrom.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugFrom.java new file mode 100644 index 000000000..e114b4e24 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugFrom.java @@ -0,0 +1,57 @@ +/* + * Copyright 2011-2024 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.issues.gh2906; + +import org.springframework.data.annotation.PersistenceCreator; +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.Relationship; +import org.springframework.data.neo4j.core.support.UUIDStringGenerator; + +/** + * @author Mathias Kühn + */ +@Node +public class BugFrom { + @Id + @GeneratedValue(UUIDStringGenerator.class) + public String uuid; + + String name; + + @Relationship(type = "RELI", direction = Relationship.Direction.INCOMING) + public IncomingBugRelationship reli; + + @PersistenceCreator // Due to the cyclic mapping you cannot have the relation as constructor parameter, how should this work? + BugFrom(String name, String uuid) { + this.name = name; + this.uuid = uuid; + } + + public BugFrom(String name, String comment, BugTargetBase target) { + this.name = name; + + this.reli = new IncomingBugRelationship(comment, target); + } + + + @Override + public String toString() { + return String.format(" {uuid: %s, name: %s}", uuid, name); + } +} + diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugRelationship.java new file mode 100644 index 000000000..7ec9aa40d --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugRelationship.java @@ -0,0 +1,47 @@ +/* + * Copyright 2011-2024 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.issues.gh2906; + +import org.springframework.data.neo4j.core.schema.RelationshipId; +import org.springframework.data.neo4j.core.schema.RelationshipProperties; +import org.springframework.data.neo4j.core.schema.TargetNode; + +/** + * @author Mathias Kühn + * @param The crux of this thing + */ +@RelationshipProperties +public abstract class BugRelationship { + + @RelationshipId + public Long id; + + public String comment; + + @TargetNode + public T target; + + BugRelationship(String comment, T target) { + this.comment = comment; + this.target = target; + } + + @Override + public String toString() { + return String.format("<%s> {id: %d, comment: %s}", this.getClass().getSimpleName(), id, comment); + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTarget.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTarget.java new file mode 100644 index 000000000..f5ec0f1c2 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTarget.java @@ -0,0 +1,33 @@ +/* + * Copyright 2011-2024 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.issues.gh2906; + +import org.springframework.data.neo4j.core.schema.Node; + +/** + * @author Mathias Kühn + */ +@Node +public class BugTarget extends BugTargetBase { + + String type; + + public BugTarget(String name, String type) { + super(name); + + this.type = type; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetBase.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetBase.java new file mode 100644 index 000000000..a939b39b1 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetBase.java @@ -0,0 +1,51 @@ +/* + * Copyright 2011-2024 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.issues.gh2906; + +import java.util.HashSet; +import java.util.Set; + +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.Relationship; +import org.springframework.data.neo4j.core.support.UUIDStringGenerator; + +/** + * @author Mathias Kühn + */ +@Node +public abstract class BugTargetBase { + + @Id + @GeneratedValue(UUIDStringGenerator.class) + public String uuid; + + public String name; + + @Relationship(type = "RELI", direction = Relationship.Direction.OUTGOING) + public Set relatedBugs = new HashSet<>(); + + BugTargetBase(String name) { + this.name = name; + } + + @Override + public String toString() { + return String.format("<%s> {uuid: %s, name: %s}", this.getClass().getSimpleName(), uuid, name); + } + +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetContainer.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetContainer.java new file mode 100644 index 000000000..2dbbb00b7 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/BugTargetContainer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2011-2024 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.issues.gh2906; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + +/** + * @author Mathias Kühn + */ +@Node +public class BugTargetContainer extends BugTargetBase { + + @Relationship(type = "INCLUDE", direction = Relationship.Direction.OUTGOING) + public Set items = new HashSet<>(); + + public BugTargetContainer(String name) { + super(name); + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/FromRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/FromRepository.java new file mode 100644 index 000000000..483469f2f --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/FromRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2011-2024 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.issues.gh2906; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +/** + * @author Gerrit Meier + */ +public interface FromRepository extends Neo4jRepository { +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/IncomingBugRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/IncomingBugRelationship.java new file mode 100644 index 000000000..fec452467 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/IncomingBugRelationship.java @@ -0,0 +1,26 @@ +/* + * Copyright 2011-2024 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.issues.gh2906; + +/** + * @author Michael J. Simons + */ +public class IncomingBugRelationship extends BugRelationship { + + IncomingBugRelationship(String comment, BugTargetBase target) { + super(comment, target); + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/OutgoingBugRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/OutgoingBugRelationship.java new file mode 100644 index 000000000..98608b802 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/OutgoingBugRelationship.java @@ -0,0 +1,26 @@ +/* + * Copyright 2011-2024 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.issues.gh2906; + +/** + * @author Michael J. Simons + */ +public class OutgoingBugRelationship extends BugRelationship { + + public OutgoingBugRelationship(String comment, BugFrom target) { + super(comment, target); + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveFromRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveFromRepository.java new file mode 100644 index 000000000..6ea76c510 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveFromRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2011-2024 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.issues.gh2906; + +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; + +/** + * @author Gerrit Meier + */ +public interface ReactiveFromRepository extends ReactiveNeo4jRepository { +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveToRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveToRepository.java new file mode 100644 index 000000000..6bdd4b462 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ReactiveToRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2011-2024 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.issues.gh2906; + +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; + +/** + * @author Gerrit Meier + */ +public interface ReactiveToRepository extends ReactiveNeo4jRepository { +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ToRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ToRepository.java new file mode 100644 index 000000000..7fb618ccb --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2906/ToRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2011-2024 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.issues.gh2906; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +/** + * @author Gerrit Meier + */ +public interface ToRepository extends Neo4jRepository { +}