GH-2618 - Allow the use of composite values as ids.
Also allow the use of composite values in derived findBy… methods. Closes #2618. # Conflicts: # src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java # src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java
This commit is contained in:
@@ -253,6 +253,22 @@ public enum CypherGenerator {
|
||||
return ongoingUpdate.build();
|
||||
}
|
||||
|
||||
public Condition createCompositePropertyCondition(GraphPropertyDescription idProperty, SymbolicName containerName, Expression actualParameter) {
|
||||
|
||||
if (!idProperty.isComposite()) {
|
||||
return Cypher.property(containerName, idProperty.getPropertyName()).isEqualTo(actualParameter);
|
||||
}
|
||||
|
||||
Neo4jPersistentProperty property = (Neo4jPersistentProperty) idProperty;
|
||||
|
||||
Condition result = Conditions.noCondition();
|
||||
for (String key : property.getOptionalConverter().write(null).keys()) {
|
||||
Property expression = Cypher.property(containerName, key);
|
||||
result = result.and(expression.isEqualTo(actualParameter.property(key)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Statement prepareSaveOf(NodeDescription<?> nodeDescription,
|
||||
UnaryOperator<OngoingMatchAndUpdate> updateDecorator) {
|
||||
|
||||
@@ -265,8 +281,7 @@ public enum CypherGenerator {
|
||||
Parameter<?> idParameter = parameter(Constants.NAME_OF_ID);
|
||||
|
||||
if (!idDescription.isInternallyGeneratedId()) {
|
||||
String nameOfIdProperty = idDescription.getOptionalGraphPropertyName()
|
||||
.orElseThrow(() -> new MappingException("External id does not correspond to a graph property"));
|
||||
GraphPropertyDescription idPropertyDescription = ((Neo4jPersistentEntity<?>) nodeDescription).getRequiredIdProperty();
|
||||
|
||||
if (((Neo4jPersistentEntity<?>) nodeDescription).hasVersionProperty()) {
|
||||
Property versionProperty = rootNode.property(((Neo4jPersistentEntity<?>) nodeDescription).getRequiredVersionProperty().getName());
|
||||
@@ -274,7 +289,7 @@ public enum CypherGenerator {
|
||||
Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode);
|
||||
|
||||
Statement createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode)
|
||||
.where(possibleExistingNode.property(nameOfIdProperty).isEqualTo(idParameter))
|
||||
.where(createCompositePropertyCondition(idPropertyDescription, possibleExistingNode.getRequiredSymbolicName(), idParameter))
|
||||
.with(possibleExistingNode)
|
||||
.where(possibleExistingNode.isNull())
|
||||
.create(rootNode.withProperties(versionProperty, literalOf(0)))
|
||||
@@ -283,7 +298,7 @@ public enum CypherGenerator {
|
||||
.build();
|
||||
|
||||
Statement updateIfExists = updateDecorator.apply(match(rootNode)
|
||||
.where(rootNode.property(nameOfIdProperty).isEqualTo(idParameter))
|
||||
.where(createCompositePropertyCondition(idPropertyDescription, rootNode.getRequiredSymbolicName(), idParameter))
|
||||
.and(versionProperty.isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM))) // Initial check
|
||||
.set(versionProperty.to(versionProperty.add(literalOf(1)))) // Acquire lock
|
||||
.with(rootNode)
|
||||
@@ -299,7 +314,7 @@ public enum CypherGenerator {
|
||||
Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode);
|
||||
|
||||
Statement createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode)
|
||||
.where(possibleExistingNode.property(nameOfIdProperty).isEqualTo(idParameter))
|
||||
.where(createCompositePropertyCondition(idPropertyDescription, possibleExistingNode.getRequiredSymbolicName(), idParameter))
|
||||
.with(possibleExistingNode)
|
||||
.where(possibleExistingNode.isNull())
|
||||
.create(rootNode)
|
||||
@@ -308,7 +323,7 @@ public enum CypherGenerator {
|
||||
.build();
|
||||
|
||||
Statement updateIfExists = updateDecorator.apply(match(rootNode)
|
||||
.where(rootNode.property(nameOfIdProperty).isEqualTo(idParameter))
|
||||
.where(createCompositePropertyCondition(idPropertyDescription, rootNode.getRequiredSymbolicName(), idParameter))
|
||||
.with(rootNode)
|
||||
.mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))
|
||||
.returning(rootNode)
|
||||
|
||||
@@ -139,6 +139,13 @@ public interface NodeDescription<T> {
|
||||
*/
|
||||
default Expression getIdExpression() {
|
||||
|
||||
if (this.getIdDescription().getOptionalGraphPropertyName()
|
||||
.flatMap(this::getGraphProperty)
|
||||
.filter(GraphPropertyDescription::isComposite)
|
||||
.isPresent()) {
|
||||
throw new IllegalStateException("A composite id property cannot be used as ID expression.");
|
||||
}
|
||||
|
||||
return this.getIdDescription().asIdExpression();
|
||||
}
|
||||
|
||||
|
||||
@@ -324,6 +324,19 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
|
||||
Neo4jPersistentProperty property = path.getRequiredLeafProperty();
|
||||
|
||||
boolean ignoreCase = ignoreCase(part);
|
||||
|
||||
if (property.isComposite()) {
|
||||
|
||||
Condition compositePropertyCondition = CypherGenerator.INSTANCE.createCompositePropertyCondition(
|
||||
property,
|
||||
Cypher.name(getContainerName(path, (Neo4jPersistentEntity<?>) property.getOwner())),
|
||||
toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
|
||||
if (part.getType() == Part.Type.NEGATING_SIMPLE_PROPERTY) {
|
||||
compositePropertyCondition = Conditions.not(compositePropertyCondition);
|
||||
}
|
||||
return compositePropertyCondition;
|
||||
}
|
||||
|
||||
return switch (part.getType()) {
|
||||
case AFTER, GREATER_THAN -> toCypherProperty(path, ignoreCase)
|
||||
.gt(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
|
||||
@@ -517,25 +530,15 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
|
||||
Neo4jPersistentEntity<?> owner = (Neo4jPersistentEntity<?>) leafProperty.getOwner();
|
||||
Expression expression;
|
||||
|
||||
String containerName = getContainerName(path, owner);
|
||||
if (owner.equals(this.nodeDescription) && path.getLength() == 1) {
|
||||
expression = leafProperty.isInternalIdProperty() ?
|
||||
Cypher.call("id").withArgs(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)).asFunction() :
|
||||
Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), leafProperty.getPropertyName());
|
||||
Cypher.property(containerName, leafProperty.getPropertyName());
|
||||
} else if (leafProperty.isInternalIdProperty()) {
|
||||
expression = Cypher.call("id").withArgs(Cypher.name(containerName)).asFunction();
|
||||
} else {
|
||||
PropertyPathWrapper propertyPathWrapper = propertyPathWrappers.stream()
|
||||
.filter(rp -> rp.getPropertyPath().equals(path)).findFirst().get();
|
||||
String cypherElementName;
|
||||
// this "entity" is a representation of a relationship with properties
|
||||
if (owner.isRelationshipPropertiesEntity()) {
|
||||
cypherElementName = propertyPathWrapper.getRelationshipName();
|
||||
} else {
|
||||
cypherElementName = propertyPathWrapper.getNodeName();
|
||||
}
|
||||
if (leafProperty.isInternalIdProperty()) {
|
||||
expression = Cypher.call("id").withArgs(Cypher.name(cypherElementName)).asFunction();
|
||||
} else {
|
||||
expression = Cypher.property(cypherElementName, leafProperty.getPropertyName());
|
||||
}
|
||||
expression = Cypher.property(containerName, leafProperty.getPropertyName());
|
||||
}
|
||||
|
||||
if (addToLower) {
|
||||
@@ -545,6 +548,24 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
|
||||
return expression;
|
||||
}
|
||||
|
||||
private String getContainerName(PersistentPropertyPath<Neo4jPersistentProperty> path, Neo4jPersistentEntity<?> owner) {
|
||||
|
||||
if (owner.equals(this.nodeDescription) && path.getLength() == 1) {
|
||||
return Constants.NAME_OF_TYPED_ROOT_NODE.apply(this.nodeDescription).getValue();
|
||||
}
|
||||
|
||||
PropertyPathWrapper propertyPathWrapper = propertyPathWrappers.stream()
|
||||
.filter(rp -> rp.getPropertyPath().equals(path)).findFirst().get();
|
||||
String cypherElementName;
|
||||
// this "entity" is a representation of a relationship with properties
|
||||
if (owner.isRelationshipPropertiesEntity()) {
|
||||
cypherElementName = propertyPathWrapper.getRelationshipName();
|
||||
} else {
|
||||
cypherElementName = propertyPathWrapper.getNodeName();
|
||||
}
|
||||
return cypherElementName;
|
||||
}
|
||||
|
||||
private Expression toCypherParameter(Parameter parameter, boolean addToLower) {
|
||||
|
||||
return createCypherParameter(parameter.nameOrIndex, addToLower);
|
||||
|
||||
@@ -63,6 +63,8 @@ class PartValidator {
|
||||
Part.Type.ENDING_WITH, Part.Type.LIKE, Part.Type.NEGATING_SIMPLE_PROPERTY, Part.Type.NOT_CONTAINING,
|
||||
Part.Type.NOT_LIKE, Part.Type.SIMPLE_PROPERTY, Part.Type.STARTING_WITH);
|
||||
|
||||
private static final EnumSet<Part.Type> TYPES_SUPPORTED_FOR_COMPOSITES = EnumSet.of(Part.Type.SIMPLE_PROPERTY, Part.Type.NEGATING_SIMPLE_PROPERTY);
|
||||
|
||||
private final Neo4jMappingContext mappingContext;
|
||||
private final Neo4jQueryMethod queryMethod;
|
||||
|
||||
@@ -80,7 +82,9 @@ class PartValidator {
|
||||
case NEAR, WITHIN -> validatePointProperty(part);
|
||||
}
|
||||
|
||||
validateNotACompositeProperty(part);
|
||||
if (!TYPES_SUPPORTED_FOR_COMPOSITES.contains(part.getType())) {
|
||||
validateNotACompositeProperty(part);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateNotACompositeProperty(Part part) {
|
||||
@@ -129,7 +133,7 @@ class PartValidator {
|
||||
* Checks whether the given part can be queried without case sensitivity.
|
||||
*
|
||||
* @param part query part to check if ignoring case sensitivity is possible
|
||||
* @return True when {@code part} can be queried case insensitive.
|
||||
* @return True when {@code part} can be queried case-insensitive.
|
||||
*/
|
||||
static boolean canIgnoreCase(Part part) {
|
||||
return part.getProperty().getLeafType() == String.class
|
||||
|
||||
@@ -17,13 +17,17 @@ package org.springframework.data.neo4j.repository.query;
|
||||
|
||||
import static org.neo4j.cypherdsl.core.Cypher.parameter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.cypherdsl.core.Condition;
|
||||
import org.neo4j.cypherdsl.core.Conditions;
|
||||
import org.neo4j.cypherdsl.core.Cypher;
|
||||
import org.neo4j.cypherdsl.core.Node;
|
||||
import org.neo4j.cypherdsl.core.SortItem;
|
||||
import org.springframework.data.domain.Example;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -94,9 +98,16 @@ public final class QueryFragmentsAndParameters {
|
||||
public static QueryFragmentsAndParameters forFindById(Neo4jPersistentEntity<?> entityMetaData, Object idValues) {
|
||||
Map<String, Object> parameters = Collections.singletonMap(Constants.NAME_OF_ID, idValues);
|
||||
|
||||
Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID));
|
||||
Node container = cypherGenerator.createRootNode(entityMetaData);
|
||||
Condition condition;
|
||||
if (entityMetaData.getIdProperty().isComposite()) {
|
||||
condition = CypherGenerator.INSTANCE.createCompositePropertyCondition(entityMetaData.getIdProperty(), container.getRequiredSymbolicName(), parameter(Constants.NAME_OF_ID));
|
||||
} else {
|
||||
condition = entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID));
|
||||
}
|
||||
|
||||
QueryFragments queryFragments = new QueryFragments();
|
||||
queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData));
|
||||
queryFragments.addMatchOn(container);
|
||||
queryFragments.setCondition(condition);
|
||||
queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData));
|
||||
return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters);
|
||||
@@ -105,9 +116,21 @@ public final class QueryFragmentsAndParameters {
|
||||
public static QueryFragmentsAndParameters forFindByAllId(Neo4jPersistentEntity<?> entityMetaData, Object idValues) {
|
||||
Map<String, Object> parameters = Collections.singletonMap(Constants.NAME_OF_IDS, idValues);
|
||||
|
||||
Condition condition = entityMetaData.getIdExpression().in((parameter(Constants.NAME_OF_IDS)));
|
||||
Node container = cypherGenerator.createRootNode(entityMetaData);
|
||||
Condition condition;
|
||||
if (entityMetaData.getIdProperty().isComposite()) {
|
||||
List<Object> args = new ArrayList<>();
|
||||
for (String key : entityMetaData.getIdProperty().getOptionalConverter().write(null).keys()) {
|
||||
args.add(key);
|
||||
args.add(container.property(key));
|
||||
}
|
||||
condition = Cypher.mapOf(args.toArray()).in(parameter(Constants.NAME_OF_IDS));
|
||||
} else {
|
||||
condition = entityMetaData.getIdExpression().in(parameter(Constants.NAME_OF_IDS));
|
||||
}
|
||||
|
||||
QueryFragments queryFragments = new QueryFragments();
|
||||
queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData));
|
||||
queryFragments.addMatchOn(container);
|
||||
queryFragments.setCondition(condition);
|
||||
queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData));
|
||||
return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters);
|
||||
@@ -124,9 +147,16 @@ public final class QueryFragmentsAndParameters {
|
||||
public static QueryFragmentsAndParameters forExistsById(Neo4jPersistentEntity<?> entityMetaData, Object idValues) {
|
||||
Map<String, Object> parameters = Collections.singletonMap(Constants.NAME_OF_ID, idValues);
|
||||
|
||||
Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID));
|
||||
Node container = cypherGenerator.createRootNode(entityMetaData);
|
||||
Condition condition;
|
||||
if (entityMetaData.getIdProperty().isComposite()) {
|
||||
condition = CypherGenerator.INSTANCE.createCompositePropertyCondition(entityMetaData.getIdProperty(), container.getRequiredSymbolicName(), parameter(Constants.NAME_OF_ID));
|
||||
} else {
|
||||
condition = entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID));
|
||||
}
|
||||
|
||||
QueryFragments queryFragments = new QueryFragments();
|
||||
queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData));
|
||||
queryFragments.addMatchOn(container);
|
||||
queryFragments.setCondition(condition);
|
||||
queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForExists(entityMetaData));
|
||||
return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters);
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2011-2022 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.conversion_imperative.compose_as_ids;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.assertj.core.data.Index;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.Session;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
|
||||
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
|
||||
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
|
||||
import org.springframework.data.neo4j.repository.Neo4jRepository;
|
||||
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
|
||||
import org.springframework.data.neo4j.test.BookmarkCapture;
|
||||
import org.springframework.data.neo4j.test.Neo4jExtension;
|
||||
import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration;
|
||||
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@Neo4jIntegrationTest
|
||||
class CompositeIdsIT {
|
||||
|
||||
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
|
||||
|
||||
interface ThingWithCompositePropertyRepository extends Neo4jRepository<ThingWithCompositeProperty, Long> {
|
||||
|
||||
Optional<ThingWithCompositeProperty> findByCompositeValue(CompositeValue compositeValue);
|
||||
|
||||
List<ThingWithCompositeProperty> findAllByCompositeValueNot(CompositeValue compositeValue);
|
||||
}
|
||||
|
||||
interface ThingWithCompositeIdRepository extends Neo4jRepository<ThingWithCompositeId, CompositeValue> {
|
||||
}
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void prepareDatabase(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) {
|
||||
|
||||
try (Session session = driver.session()) {
|
||||
session.run("MATCH (n:ThingWithCompositeProperty) DETACH DELETE n").consume();
|
||||
session.run("MATCH (n:ThingWithCompositeId) DETACH DELETE n").consume();
|
||||
bookmarkCapture.seedWith(session.lastBookmark());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByCompositeValuesShouldWork(@Autowired ThingWithCompositePropertyRepository repository) {
|
||||
|
||||
ThingWithCompositeProperty thing = new ThingWithCompositeProperty(new CompositeValue("a", 1), "first entity");
|
||||
ThingWithCompositeProperty saved = repository.save(thing);
|
||||
|
||||
repository.save(new ThingWithCompositeProperty(new CompositeValue("b", 1), "2nd entity"));
|
||||
|
||||
saved.setName("foobar");
|
||||
saved = repository.save(saved);
|
||||
assertThat(saved.getName()).isEqualTo("foobar");
|
||||
|
||||
Optional<ThingWithCompositeProperty> reloaded = repository.findByCompositeValue(saved.getCompositeValue());
|
||||
assertThat(reloaded).hasValueSatisfying(v -> assertThat(v.getName()).isEqualTo("foobar"));
|
||||
|
||||
assertThat(repository.findAllByCompositeValueNot(saved.getCompositeValue()))
|
||||
.hasSize(1)
|
||||
.element(0)
|
||||
.satisfies(v -> assertThat(v.getCompositeValue()).isEqualTo(new CompositeValue("b", 1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void compositeIdsShouldWork(@Autowired ThingWithCompositeIdRepository repository) {
|
||||
|
||||
ThingWithCompositeId thing = new ThingWithCompositeId(new CompositeValue("a,", 1), "first entity");
|
||||
ThingWithCompositeId saved = repository.save(thing);
|
||||
assertThat(saved.getVersion()).isGreaterThanOrEqualTo(0);
|
||||
|
||||
saved.setName("foobar");
|
||||
saved = repository.save(saved);
|
||||
assertThat(saved.getVersion()).isGreaterThan(0);
|
||||
assertThat(saved.getName()).isEqualTo("foobar");
|
||||
|
||||
Optional<ThingWithCompositeId> reloaded = repository.findById(saved.getId());
|
||||
assertThat(reloaded).isPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findAllByCompositeIdsShouldWork(@Autowired ThingWithCompositeIdRepository repository) {
|
||||
|
||||
int cnt = 0;
|
||||
String[] value1Values = {"a", "b"};
|
||||
int[] value2Values = {1, 2};
|
||||
List<CompositeValue> ids = new ArrayList<>(value1Values.length * value2Values.length);
|
||||
for (String value1 : value1Values) {
|
||||
for (int value2 : value2Values) {
|
||||
CompositeValue id = new CompositeValue(value1, value2);
|
||||
ids.add(id);
|
||||
ThingWithCompositeId saved = repository.save(new ThingWithCompositeId(id, "Entity " + ++cnt));
|
||||
assertThat(saved.getVersion()).isGreaterThanOrEqualTo(0);
|
||||
}
|
||||
}
|
||||
CompositeValue removedId = ids.remove(ids.size() - 1);
|
||||
|
||||
List<ThingWithCompositeId> loadedThings = repository.findAllById(ids);
|
||||
Collections.sort(loadedThings, Comparator.comparing(ThingWithCompositeId::getName));
|
||||
assertThat(loadedThings)
|
||||
.hasSize(ids.size())
|
||||
.satisfies(v -> assertThat(v.getName()).isEqualTo("Entity 1"), Index.atIndex(0))
|
||||
.satisfies(v -> assertThat(v.getName()).isEqualTo("Entity 3"), Index.atIndex(2));
|
||||
|
||||
assertThat(repository.existsById(removedId)).isTrue();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
@EnableNeo4jRepositories(considerNestedRepositories = true)
|
||||
static class Config extends Neo4jImperativeTestConfiguration {
|
||||
|
||||
@Bean
|
||||
public Driver driver() {
|
||||
return neo4jConnectionSupport.getDriver();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<String> getMappingBasePackages() {
|
||||
return Collections.singleton(CompositeIdsIT.class.getPackage().getName());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BookmarkCapture bookmarkCapture() {
|
||||
return new BookmarkCapture();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) {
|
||||
|
||||
BookmarkCapture bookmarkCapture = bookmarkCapture();
|
||||
return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCypher5Compatible() {
|
||||
return neo4jConnectionSupport.isCypher5SyntaxCompatible();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2011-2022 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.conversion_imperative.compose_as_ids;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
|
||||
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyToMapConverter;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @param value1 some value
|
||||
* @param value2 some value, too
|
||||
*/
|
||||
public record CompositeValue(String value1, Integer value2) {
|
||||
|
||||
static class Converter implements Neo4jPersistentPropertyToMapConverter<String, CompositeValue> {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, Value> decompose(@Nullable CompositeValue property, Neo4jConversionService conversionService) {
|
||||
|
||||
final HashMap<String, Value> decomposed = new HashMap<>();
|
||||
if (property == null) {
|
||||
decomposed.put("value1", Values.NULL);
|
||||
decomposed.put("value2", Values.NULL);
|
||||
} else {
|
||||
decomposed.put("value1", Values.value(property.value1));
|
||||
decomposed.put("value2", Values.value(property.value2));
|
||||
}
|
||||
return decomposed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompositeValue compose(Map<String, Value> source, Neo4jConversionService conversionService) {
|
||||
return source.isEmpty() ?
|
||||
null :
|
||||
new CompositeValue(source.get("value1").asString(), source.get("value2").asInt());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2011-2022 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.conversion_imperative.compose_as_ids;
|
||||
|
||||
import org.springframework.data.annotation.Version;
|
||||
import org.springframework.data.neo4j.core.schema.CompositeProperty;
|
||||
import org.springframework.data.neo4j.core.schema.Id;
|
||||
import org.springframework.data.neo4j.core.schema.Node;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@Node
|
||||
public class ThingWithCompositeId {
|
||||
|
||||
@Id
|
||||
@CompositeProperty(converter = CompositeValue.Converter.class)
|
||||
private final CompositeValue id;
|
||||
|
||||
@Version
|
||||
private Long version;
|
||||
|
||||
private String name;
|
||||
|
||||
public ThingWithCompositeId(CompositeValue id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public CompositeValue getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Long getVersion() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2011-2022 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.conversion_imperative.compose_as_ids;
|
||||
|
||||
import org.springframework.data.neo4j.core.schema.CompositeProperty;
|
||||
import org.springframework.data.neo4j.core.schema.GeneratedValue;
|
||||
import org.springframework.data.neo4j.core.schema.Id;
|
||||
import org.springframework.data.neo4j.core.schema.Node;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@Node
|
||||
public class ThingWithCompositeProperty {
|
||||
|
||||
@Id @GeneratedValue
|
||||
private Long id;
|
||||
|
||||
@CompositeProperty(converter = CompositeValue.Converter.class)
|
||||
private final CompositeValue compositeValue;
|
||||
|
||||
private String name;
|
||||
|
||||
public ThingWithCompositeProperty(CompositeValue compositeValue, String name) {
|
||||
this.compositeValue = compositeValue;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public CompositeValue getCompositeValue() {
|
||||
return compositeValue;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user