Add @Id and friends.

This commit is contained in:
Michael Simons
2019-03-27 00:23:40 +00:00
parent 33bf99dcc4
commit d310d83279
9 changed files with 251 additions and 19 deletions

26
etc/adr/adr-001.adoc Normal file
View File

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

View File

@@ -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<Neo4jPersistentE
}
});
return new NodeDescription(entity.getPrimaryLabel(), properties, relationships);
final Neo4jPersistentProperty idProperty = entity.getRequiredIdProperty();
final Optional<Id> 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();
}

View File

@@ -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<? extends IdGenerator> generator() default IdDescription.NoopIdGenerator.class;
}

View File

@@ -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<? extends IdGenerator> idGeneratorClass;
public IdDescription() {
this(Id.Strategy.INTERNAL, NoopIdGenerator.class);
}
public IdDescription(Id.Strategy idStrategy, Class<? extends IdGenerator> idGeneratorClass) {
this.idStrategy = idStrategy;
this.idGeneratorClass = idGeneratorClass;
}
public Id.Strategy getIdStrategy() {
return idStrategy;
}
public Class<? extends IdGenerator> getIdGeneratorClass() {
return idGeneratorClass;
}
static class NoopIdGenerator implements IdGenerator {
@Override
public Object generateId(Object entity) {
return null;
}
}
}

View File

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

View File

@@ -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<String, PropertyDescription> propertiesByFieldName;
private final List<RelationshipDescription> relationships;
public NodeDescription(String primaryLabel, List<PropertyDescription> properties,
@Builder
private NodeDescription(String primaryLabel, IdDescription idDescription,
List<PropertyDescription> properties,
List<RelationshipDescription> 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
*/

View File

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

View File

@@ -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<NodeDescription> 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<NodeDescription> 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<BikeNode> bikes;
@@ -85,10 +96,11 @@ class Neo4jMappingContextTest {
static class BikeNode {
@Id(strategy = Id.Strategy.ASSIGNED)
private String id;
UserNode owner;
List<UserNode> renter;
}
}

View File

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