diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java index 3a5d06b91..8ee5c567e 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java @@ -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 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) diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescription.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescription.java index 588d04822..66f40d9e6 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescription.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescription.java @@ -139,6 +139,13 @@ public interface NodeDescription { */ 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(); } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index 0e6dcb2a1..9fd7cee35 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -324,6 +324,19 @@ final class CypherQueryCreator extends AbstractQueryCreator) 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 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 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); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/PartValidator.java b/src/main/java/org/springframework/data/neo4j/repository/query/PartValidator.java index a56f8277b..cbede29fa 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/PartValidator.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/PartValidator.java @@ -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 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 diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java index 84ad15a07..1153d8890 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java @@ -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 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 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 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 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); diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeIdsIT.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeIdsIT.java new file mode 100644 index 000000000..209784550 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeIdsIT.java @@ -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 { + + Optional findByCompositeValue(CompositeValue compositeValue); + + List findAllByCompositeValueNot(CompositeValue compositeValue); + } + + interface ThingWithCompositeIdRepository extends Neo4jRepository { + } + + + @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 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 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 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 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 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(); + } + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeValue.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeValue.java new file mode 100644 index 000000000..3da3b39f8 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeValue.java @@ -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 { + + @NonNull + @Override + public Map decompose(@Nullable CompositeValue property, Neo4jConversionService conversionService) { + + final HashMap 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 source, Neo4jConversionService conversionService) { + return source.isEmpty() ? + null : + new CompositeValue(source.get("value1").asString(), source.get("value2").asInt()); + } + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeId.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeId.java new file mode 100644 index 000000000..1d1fffb8d --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeId.java @@ -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; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeProperty.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeProperty.java new file mode 100644 index 000000000..92db01f30 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/ThingWithCompositeProperty.java @@ -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; + } + +}