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:
Michael Simons
2022-10-28 13:56:20 +02:00
parent 8464d1d807
commit 0738bc03f7
9 changed files with 449 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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