diff --git a/etc/adr/adr-001.adoc b/etc/adr/adr-001.adoc new file mode 100644 index 000000000..83c2b3614 --- /dev/null +++ b/etc/adr/adr-001.adoc @@ -0,0 +1,26 @@ +== ADR 1: Configuration of Id-Mapping + +=== Status + +accepted + +=== Context + +SDN RX needs to provide a configuration of Ids mappings. +Ids can either be internal (native Neo4j) Ids or generated Ids. + +=== Decision + +The configuration should not be ambiguous. +`@org.springframework.data.annotation.Id` will be used as the marker for an Id. +A _strategy_ will be used to decide whether the annotated attribute will be mapped to `id(node)` or set from the external. +The strategy will either be `internal`, `assigned` or `generated`. +`generated` will require an additional attribute of `generator`. + +The internal strategy will be the default. + +To configure the Id strategy, a meta-annotated `@Id` annotation will be provided through `org.springframework.data.neo4j.core.schema.Id` + +=== Consequences + +We are still compatible with the OGM 3.1+ approach of recommending `@Id long id;` while providing a clear direction for the user. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 967b0a1ba..0fff776cb 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -20,7 +20,9 @@ package org.springframework.data.neo4j.core.mapping; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.SimpleAssociationHandler; @@ -28,6 +30,8 @@ import org.springframework.data.mapping.SimplePropertyHandler; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.IdDescription; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.core.schema.PropertyDescription; import org.springframework.data.neo4j.core.schema.Relationship; @@ -118,7 +122,19 @@ public class Neo4jMappingContext extends AbstractMappingContext optionalIdAnnotation = Optional + .ofNullable(AnnotatedElementUtils.findMergedAnnotation(idProperty.getField(), Id.class)); + final IdDescription idDescription = optionalIdAnnotation + .map(idAnnotation -> new IdDescription(idAnnotation.strategy(), idAnnotation.generator())) + .orElseGet(() -> new IdDescription()); + + return NodeDescription.builder() + .primaryLabel(entity.getPrimaryLabel()) + .idDescription(idDescription) + .properties(properties) + .relationships(relationships) + .build(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java new file mode 100644 index 000000000..ae6bcf2b1 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Id.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 + * + * http://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.core.schema; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * Annotation to configure assigment of ids. + * + * @author Michael J. Simons + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Documented +@Inherited +@org.springframework.data.annotation.Id +@API(status = API.Status.STABLE, since = "1.0") +public @interface Id { + + enum Strategy { + + /** + * The default, use Neo4js internal ids. + */ + INTERNAL, + + /** + * Use assigned values. + */ + ASSIGNED, + + /** + * Use generated values, generator class is required. + */ + GENERATED + } + + Strategy strategy() default Strategy.INTERNAL; + + Class generator() default IdDescription.NoopIdGenerator.class; +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java new file mode 100644 index 000000000..f09c13fe6 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdDescription.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 + * + * http://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.core.schema; + +import org.apiguardian.api.API; + +/** + * Description howto generate Ids for entities. + * + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class IdDescription { + + private final Id.Strategy idStrategy; + + private final Class idGeneratorClass; + + public IdDescription() { + this(Id.Strategy.INTERNAL, NoopIdGenerator.class); + } + + public IdDescription(Id.Strategy idStrategy, Class idGeneratorClass) { + this.idStrategy = idStrategy; + this.idGeneratorClass = idGeneratorClass; + } + + public Id.Strategy getIdStrategy() { + return idStrategy; + } + + public Class getIdGeneratorClass() { + return idGeneratorClass; + } + + static class NoopIdGenerator implements IdGenerator { + + @Override + public Object generateId(Object entity) { + return null; + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java new file mode 100644 index 000000000..52d6232b1 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IdGenerator.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 + * + * http://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.core.schema; + +import org.apiguardian.api.API; + +/** + * Interface for generating ids for entities. + * + * @author Michael J. Simons + */ +@FunctionalInterface +@API(status = API.Status.STABLE, since = "1.0") +public interface IdGenerator { + + /** + * Generates a new id for given entity + * + * @param entity the entity to be saved + * @return id to be assigned to the entity + */ + Object generateId(Object entity); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java index 0fe112b49..1545709fd 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java @@ -18,6 +18,8 @@ */ package org.springframework.data.neo4j.core.schema; +import lombok.Builder; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -43,16 +45,20 @@ public final class NodeDescription { */ private final String primaryLabel; + private final IdDescription idDescription; + private final Map propertiesByFieldName; private final List relationships; - public NodeDescription(String primaryLabel, List properties, + @Builder + private NodeDescription(String primaryLabel, IdDescription idDescription, + List properties, List relationships) { this.primaryLabel = primaryLabel; + this.idDescription = idDescription; this.propertiesByFieldName = properties.stream() - .collect(Collectors.toMap(PropertyDescription::getFieldName, Function - .identity())); + .collect(Collectors.toMap(PropertyDescription::getFieldName, Function.identity())); this.relationships = new ArrayList<>(relationships); } @@ -63,6 +69,10 @@ public final class NodeDescription { return primaryLabel; } + public IdDescription getIdDescription() { + return idDescription; + } + /** * @return The properties of this node */ diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java index 732ac4049..fd91a3822 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java @@ -18,15 +18,12 @@ */ package org.springframework.data.neo4j.core.schema; -import lombok.Getter; - import org.apiguardian.api.API; /** * @author Michael J. Simons */ @API(status = API.Status.INTERNAL, since = "1.0") -@Getter public final class PropertyDescription { private final String fieldName; @@ -41,4 +38,12 @@ public final class PropertyDescription { this.fieldName = fieldName; this.propertyName = propertyName; } + + public String getFieldName() { + return fieldName; + } + + public String getPropertyName() { + return propertyName; + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java index 511177caf..ea9993e4b 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; +import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.NodeDescription; import org.springframework.data.neo4j.core.schema.Property; @@ -51,29 +52,39 @@ class Neo4jMappingContextTest { Optional optionalUserNodeDescription = schema.getNodeDescription("User"); assertThat(optionalUserNodeDescription) .isPresent() - .hasValueSatisfying(userNodeDescription -> { - assertThat(userNodeDescription.getProperties()) - .extracting(PropertyDescription::getFieldName) - .containsExactlyInAnyOrder("name", "first_name"); + .hasValueSatisfying(description -> { + assertThat(description.getIdDescription().getIdStrategy()) + .isEqualTo(Id.Strategy.INTERNAL); - assertThat(userNodeDescription.getProperties()) + assertThat(description.getProperties()) + .extracting(PropertyDescription::getFieldName) + .containsExactlyInAnyOrder("id", "name", "first_name"); + + assertThat(description.getProperties()) .extracting(PropertyDescription::getPropertyName) - .containsExactlyInAnyOrder("name", "firstName"); + .containsExactlyInAnyOrder("id", "name", "firstName"); }); Optional optionalBikeNodeDescription = schema.getNodeDescription("BikeNode"); assertThat(optionalBikeNodeDescription) .isPresent() - .hasValueSatisfying(bikeNodeDescription -> - assertThat(bikeNodeDescription.getRelationships()) + .hasValueSatisfying(description -> { + assertThat(description.getIdDescription().getIdStrategy()) + .isEqualTo(Id.Strategy.ASSIGNED); + + assertThat(description.getRelationships()) .containsExactlyInAnyOrder( new RelationshipDescription("owner", "User"), - new RelationshipDescription("renter", "User"))); + new RelationshipDescription("renter", "User")); + }); } @Node("User") static class UserNode { + @org.springframework.data.annotation.Id + private long id; + @Relationship(type = "OWNS", inverse = "owner") List bikes; @@ -85,10 +96,11 @@ class Neo4jMappingContextTest { static class BikeNode { + @Id(strategy = Id.Strategy.ASSIGNED) + private String id; + UserNode owner; List renter; } - - } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java index 1defe9f7e..000353487 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java @@ -31,7 +31,8 @@ class SchemaTest { @Test void shouldGetNodeDescription() { - NodeDescription description = new NodeDescription("aLabel", Collections.emptyList(), Collections.emptyList()); + NodeDescription description = NodeDescription.builder().primaryLabel("aLabel") + .properties(Collections.emptyList()).relationships(Collections.emptyList()).build(); Schema schema = new Schema().registerNodeDescription(description); assertThat(schema.getNodeDescription("aLabel")).isPresent().contains(description);