feat: Allow bidirectional mapping of relationship with properties. (#2914)

This will allow for the mapper to have only *one* physical relationship plus the original behaviour staying intact (creating two independent).

Required mapping is shown in the test.

Basic idea is to check if a relationship in the opposite direction with the actual *same* source and target entities has already been seen. If so, no batch update on the imperative path is scheduled.
Thus however will leave generated ids on the mapping classes unpopulated.
Those will be retrieved after the fact.

---------
Co-authored-by: Gerrit Meier <meistermeier@gmail.com>
This commit is contained in:
Michael Simons
2024-06-25 06:48:39 +02:00
parent bc9ac76000
commit 46e5b39fc2
27 changed files with 1684 additions and 54 deletions

View File

@@ -883,7 +883,7 @@ public final class Neo4jTemplate implements
List<Map<String, Object>> relationshipPropertiesRows = new ArrayList<>();
List<Map<String, Object>> newRelationshipPropertiesRows = new ArrayList<>();
List<Object> updateRelatedValuesToStore = new ArrayList<>();
List<Object> newRelatedValuesToStore = new ArrayList<>();
List<Object> 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<Object> 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<Object> 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) {

View File

@@ -418,19 +418,25 @@ public final class ReactiveNeo4jTemplate implements
getProjectionFactory(), neo4jMappingContext);
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
Collection<Object> knownRelationshipsIds = new HashSet<>();
EntityFromDtoInstantiatingConverter<T> converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext);
return Flux.fromIterable(instances)
.concatMap(instance -> {
T domainObject = converter.convert(instance);
@SuppressWarnings("unchecked")
Mono<R> result = transactionalOperator.transactional(saveImpl(domainObject, pps, stateMachine)
Mono<R> result = transactionalOperator.transactional(saveImpl(domainObject, pps, stateMachine, knownRelationshipsIds)
.map(savedEntity -> (R) new DtoInstantiatingConverter(resultType, neo4jMappingContext).convertDirectly(savedEntity)));
return result;
});
}
private <T> Mono<T> saveImpl(T instance, @Nullable Collection<PropertyFilter.ProjectedPath> includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) {
return saveImpl(instance, includedProperties, stateMachine, new HashSet<>());
}
private <T> Mono<T> saveImpl(T instance, @Nullable Collection<PropertyFilter.ProjectedPath> includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine, Collection<Object> 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<T, Map<String, Object>> binderFunction = TemplateSupport.createAndApplyPropertyFilter(
pps, entityMetaData,
neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) domainClass));
return Flux.fromIterable(entities)
return (Flux<T>) Flux.deferContextual((ctx) -> Flux.fromIterable(entities)
// Map all entities into a tuple <Original, OriginalWasNew>
.map(e -> Tuples.of(e, entityMetaData.isNew(e)))
// Map that tuple into a tuple <<Original, OriginalWasNew>, PotentiallyModified>
@@ -618,11 +624,16 @@ public final class ReactiveNeo4jTemplate implements
.concatMap(t -> {
PersistentPropertyAccessor<T> 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<Object> knownRelationshipsIds,
PropertyFilter includeProperty
) {
PropertyFilter.RelaxedPropertyPath startingPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(neo4jPersistentEntity.getUnderlyingClass());
return processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew,
stateMachine, includeProperty, startingPropertyPath);
stateMachine, knownRelationshipsIds, includeProperty, startingPropertyPath);
}
private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity, PersistentPropertyAccessor<?> parentPropertyAccessor,
boolean isParentObjectNew, NestedRelationshipProcessingStateMachine stateMachine, PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath previousPath) {
boolean isParentObjectNew, NestedRelationshipProcessingStateMachine stateMachine,
Collection<Object> knownRelationshipsIds,
PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath previousPath) {
Object fromId = parentPropertyAccessor.getProperty(sourceEntity.getRequiredIdProperty());
List<Mono<Void>> relationshipDeleteMonos = new ArrayList<>();
@@ -932,7 +946,6 @@ public final class ReactiveNeo4jTemplate implements
boolean canUseElementId = TemplateSupport.rendererRendersElementId(renderer);
if (!isParentObjectNew && !stateMachine.hasProcessedRelationship(fromId, relationshipDescription)) {
List<Object> 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<Object> 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<Object> nestedRelationshipsSignal = null;
if (processState != ProcessState.PROCESSED_ALL_VALUES) {
nestedRelationshipsSignal = processNestedRelations(targetEntity, targetPropertyAccessor, targetEntity.isNew(newRelatedObject), stateMachine, includeProperty, currentPropertyPath);
}
Mono<Object> nestedRelationshipsSignal = null;
if (processState != ProcessState.PROCESSED_ALL_VALUES) {
nestedRelationshipsSignal = processNestedRelations(targetEntity, targetPropertyAccessor, targetEntity.isNew(newRelatedObject), stateMachine, knownRelationshipsIds, includeProperty, currentPropertyPath);
}
Mono<Object> getRelationshipOrRelationshipPropertiesObject = Mono.fromSupplier(() -> MappingSupport.getRelationshipOrRelationshipPropertiesObject(
neo4jMappingContext,
relationshipDescription.hasRelationshipProperties(),
relationshipProperty.isDynamicAssociation(),
relatedValueToStore,
targetPropertyAccessor));
return nestedRelationshipsSignal == null ? getRelationshipOrRelationshipPropertiesObject :
nestedRelationshipsSignal.then(getRelationshipOrRelationshipPropertiesObject);
});
Mono<Object> 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<Object> 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<Entity> loadRelatedNode(NodeDescription<?> targetNodeDescription, Object relatedInternalId) {

View File

@@ -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)

View File

@@ -603,7 +603,7 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
Statement relationshipCreationQuery = CypherGenerator.INSTANCE.prepareSaveOfRelationshipWithProperties(
neo4jPersistentEntity, relationshipDescription, isNewRelationship,
dynamicRelationshipType, canUseElementId);
dynamicRelationshipType, canUseElementId, false);
Map<String, Object> propMap = new HashMap<>();
// write relationship properties

View File

@@ -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<Integer, Object> processedObjectsIds = new HashMap<>();
private final Set<ProcessedRelationshipEntity> processedRelationshipEntities = new HashSet<>();
private final Set<RelationshipIdUpdateContext> 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<Object> getId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId);
}
/**
* Reactive Supplier for arbitrary relationship ids
*/
@FunctionalInterface
public interface ReactiveRelationshipIdSupplier {
Mono<Object> 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<RelationshipIdUpdateContext> 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<Void> 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 {

View File

@@ -399,7 +399,7 @@ class OptimisticLockingIT {
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
});
});

View File

@@ -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<String> 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")

View File

@@ -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<String> 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<BugFrom> 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<BugFrom> 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")

View File

@@ -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 + ")";
}
}
}

View File

@@ -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 + ")";
}
}
}

View File

@@ -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<BugFromV1> relatedBugs;
BugTargetBaseV1(String uuid, String name, Set<BugFromV1> relatedBugs) {
this.uuid = uuid;
this.name = name;
this.relatedBugs = relatedBugs;
}
}

View File

@@ -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<BugFromV1> 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<BugFromV1> 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<BugFromV1> 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 + ")";
}
}
}

View File

@@ -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<BugFromV1, String> {
}

View File

@@ -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<BugFromV1, String> {
}

View File

@@ -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<BugTargetV1, String> {
}

View File

@@ -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<BugTargetV1, String> {
}

View File

@@ -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("<BugFrom> {uuid: %s, name: %s}", uuid, name);
}
}

View File

@@ -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 <T> The crux of this thing
*/
@RelationshipProperties
public abstract class BugRelationship<T> {
@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);
}
}

View File

@@ -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;
}
}

View File

@@ -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<OutgoingBugRelationship> 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);
}
}

View File

@@ -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<BugTargetBase> items = new HashSet<>();
public BugTargetContainer(String name) {
super(name);
}
}

View File

@@ -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<BugFrom, String> {
}

View File

@@ -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<BugTargetBase> {
IncomingBugRelationship(String comment, BugTargetBase target) {
super(comment, target);
}
}

View File

@@ -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<BugFrom> {
public OutgoingBugRelationship(String comment, BugFrom target) {
super(comment, target);
}
}

View File

@@ -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<BugFrom, String> {
}

View File

@@ -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<BugTargetBase, String> {
}

View File

@@ -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<BugTargetBase, String> {
}