GH-2371 - Respect @Property information in projection.

Domain attributes can be renamed using the `@Property` annotation when
being mapped to and read from graph properties. That renaming must also
happening when using projections. As with #2451 in
582fd7de84 this is kind of a problem with
the existing filter mechanism that works after the stack has dealt with
the actual properties.

After all properties and paths are computed, they will be translated as
late as possible before checked for inclusions:

- When writing just before it is checked whether to add them to the
  properties parameter or not
- When reading after they have been map-projected from the database into
  the result set but before they are checked for inclusions.

Also addressed here is the a workaround for writes: In case a user had
an interface based projection and used the graph property name as
accessor, a write would succeed, however a read would fail. A property
accessor has now been registered with the default projection factory
that translates a failure to access an entity property (attribute) once.

This fixes #2371.

# Conflicts:
#	src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveProjectionIT.java
This commit is contained in:
Michael Simons
2022-02-01 14:54:02 +01:00
parent ee10bc4e85
commit 08d834dbd8
9 changed files with 471 additions and 29 deletions

View File

@@ -15,16 +15,18 @@
*/
package org.springframework.data.neo4j.core.mapping;
import org.apiguardian.api.API;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.util.StringUtils;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apiguardian.api.API;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.neo4j.core.schema.Property;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* Something that makes sense of propertyPaths by having an understanding of projection classes.
*/
@@ -45,6 +47,24 @@ public abstract class PropertyFilter {
public abstract boolean isNotFiltering();
static String toDotPath(PropertyPath propertyPath, @Nullable String lastSegment) {
if (lastSegment == null) {
return propertyPath.toDotPath();
}
StringBuilder dotPath = new StringBuilder();
while (propertyPath != null) {
if (propertyPath.hasNext()) {
dotPath.append(propertyPath.getSegment()).append(".");
propertyPath = propertyPath.next();
} else {
break;
}
}
dotPath.append(lastSegment);
return dotPath.toString();
}
private static class FilteringPropertyFilter extends PropertyFilter {
private final Set<Class<?>> rootClasses;
private final Map<String, Boolean> projectingPropertyPaths;
@@ -64,10 +84,23 @@ public abstract class PropertyFilter {
.map(NodeDescription::getUnderlyingClass)
.forEach(rootClasses::add);
Neo4jPersistentEntity<?> entity = (Neo4jPersistentEntity<?>) nodeDescription;
projectingPropertyPaths = new ConcurrentHashMap<>();
propertiesMap.keySet()
.forEach(propertyPath ->
projectingPropertyPaths.put(propertyPath.toDotPath(), propertiesMap.get(propertyPath)));
.forEach(propertyPath -> {
String lastSegment = null;
if (!propertyPath.hasNext()) {
Neo4jPersistentProperty property = entity.getPersistentProperty(
propertyPath.getLeafProperty().getSegment());
if (property != null && property.findAnnotation(Property.class) != null) {
lastSegment = property.getPropertyName();
}
}
projectingPropertyPaths.put(
PropertyFilter.toDotPath(propertyPath, lastSegment),
propertiesMap.get(propertyPath)
);
});
}
@Override
@@ -87,7 +120,8 @@ public abstract class PropertyFilter {
return Integer.compare(depth2, depth1);
})
.filter(d -> dotPath.contains(d) && dotPath.startsWith(d)).findFirst();
.filter(d -> dotPath.contains(d) && dotPath.startsWith(d))
.findFirst();
return projectingPropertyPaths.containsKey(dotPath)
|| (dotPath.contains(".") && candidate.isPresent() && projectingPropertyPaths.get(candidate.get()));
@@ -141,6 +175,19 @@ public abstract class PropertyFilter {
return dotPath;
}
public String toDotPath(@Nullable String lastSegment) {
if (lastSegment == null) {
return this.toDotPath();
}
int idx = dotPath.lastIndexOf('.');
if (idx < 0) {
return lastSegment;
}
return dotPath.substring(0, idx + 1) + lastSegment;
}
public Class<?> getType() {
return type;
}
@@ -165,6 +212,24 @@ public abstract class PropertyFilter {
private String prependDotPathWith(String pathPart) {
return dotPath.isEmpty() ? pathPart : pathPart + "." + dotPath;
}
}
public String getSegment() {
int idx = dotPath.indexOf(".");
if (idx < 0) {
idx = dotPath.length();
}
return dotPath.substring(0, idx);
}
public RelaxedPropertyPath getLeafProperty() {
int idx = dotPath.lastIndexOf('.');
if (idx < 0) {
return this;
}
return new RelaxedPropertyPath(dotPath.substring(idx + 1), this.type);
}
}
}

View File

@@ -33,9 +33,11 @@ import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.StatementBuilder;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.core.mapping.NodeDescription;
import org.springframework.data.neo4j.core.schema.Property;
import org.springframework.lang.Nullable;
/**
@@ -86,9 +88,7 @@ public final class QueryFragments {
}
public boolean includeField(PropertyFilter.RelaxedPropertyPath fieldName) {
return this.returnTuple == null
? PropertyFilter.acceptAll().contains(fieldName.toDotPath(), fieldName.getType())
: this.returnTuple.filteredProperties.contains(fieldName.toDotPath(), fieldName.getType());
return this.returnTuple == null || this.returnTuple.include(fieldName);
}
public void setOrderBy(Collection<SortItem> orderBy) {
@@ -179,5 +179,15 @@ public final class QueryFragments {
this.filteredProperties = PropertyFilter.from(filteredProperties, nodeDescription);
this.isDistinct = isDistinct;
}
boolean include(PropertyFilter.RelaxedPropertyPath fieldName) {
String dotPath = nodeDescription.getGraphProperty(fieldName.getSegment())
.filter(Neo4jPersistentProperty.class::isInstance)
.map(Neo4jPersistentProperty.class::cast)
.filter(p -> p.findAnnotation(Property.class) != null)
.map(p -> fieldName.toDotPath(p.getPropertyName()))
.orElseGet(fieldName::toDotPath);
return this.filteredProperties.contains(dotPath, fieldName.getType());
}
}
}

View File

@@ -0,0 +1,139 @@
/*
* 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.repository.support;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicReference;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.NotReadablePropertyException;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.core.schema.Property;
import org.springframework.data.projection.MethodInterceptorFactory;
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Basically a lenient property accessing method interceptor, first trying the entity property (or attribute), than
* a potentially renamed attribute via {@link Property}.
*
* @author Michael J. Simons
*/
final class EntityAndGraphPropertyAccessingMethodInterceptor implements MethodInterceptor {
static MethodInterceptorFactory createMethodInterceptorFactory(Neo4jMappingContext mappingContext) {
return new MethodInterceptorFactory() {
@Override
public MethodInterceptor createMethodInterceptor(Object source, Class<?> targetType) {
return new EntityAndGraphPropertyAccessingMethodInterceptor(source, mappingContext);
}
@Override public boolean supports(Object source, Class<?> targetType) {
return true;
}
};
}
private final BeanWrapper target;
private EntityAndGraphPropertyAccessingMethodInterceptor(Object target, Neo4jMappingContext ctx) {
Assert.notNull(target, "Proxy target must not be null!");
this.target = new GraphPropertyAndDirectFieldAccessFallbackBeanWrapper(target, ctx);
}
@Nullable
@Override
public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
if (ReflectionUtils.isObjectMethod(method)) {
return invocation.proceed();
}
PropertyDescriptor descriptor = BeanUtils.findPropertyForMethod(method);
if (descriptor == null) {
throw new IllegalStateException("Invoked method is not a property accessor!");
}
if (!isSetterMethod(method, descriptor)) {
return target.getPropertyValue(descriptor.getName());
}
if (invocation.getArguments().length != 1) {
throw new IllegalStateException("Invoked setter method requires exactly one argument!");
}
target.setPropertyValue(descriptor.getName(), invocation.getArguments()[0]);
return null;
}
private static boolean isSetterMethod(Method method, PropertyDescriptor descriptor) {
return method.equals(descriptor.getWriteMethod());
}
/**
* this version of the {@link DirectFieldAccessFallbackBeanWrapper} checks if there's an attribute on the entity
* annotated with {@link Property} mapping it to a different graph property when it fails to access the original
* attribute If so, that property is accessed. If not, the original exception is rethrown.
* This helps in projections such as described here
* https://stackoverflow.com/questions/68938823/sdn6-projection-interfaces-with-property-mapping
* that could have been used as workaround prior to fixing 2371.
*/
static class GraphPropertyAndDirectFieldAccessFallbackBeanWrapper extends DirectFieldAccessFallbackBeanWrapper {
private final Neo4jMappingContext ctx;
GraphPropertyAndDirectFieldAccessFallbackBeanWrapper(Object target, Neo4jMappingContext ctx) {
super(target);
this.ctx = ctx;
}
@Override
public Object getPropertyValue(String propertyName) {
try {
return super.getPropertyValue(propertyName);
} catch (NotReadablePropertyException e) {
Neo4jPersistentEntity<?> entity = ctx.getPersistentEntity(super.getRootClass());
AtomicReference<String> value = new AtomicReference<>();
if (entity != null) {
entity.doWithProperties(
(org.springframework.data.mapping.PropertyHandler<Neo4jPersistentProperty>) p -> {
if (p.findAnnotation(Property.class) != null && p.getPropertyName()
.equals(propertyName)) {
value.compareAndSet(null, p.getFieldName());
}
});
if (value.get() != null) {
return super.getPropertyValue(value.get());
}
}
throw e;
}
}
}
}

View File

@@ -25,6 +25,8 @@ import org.springframework.data.neo4j.repository.query.CypherdslConditionExecuto
import org.springframework.data.neo4j.repository.query.Neo4jQueryLookupStrategy;
import org.springframework.data.neo4j.repository.query.QuerydslNeo4jPredicateExecutor;
import org.springframework.data.neo4j.repository.query.SimpleQueryByExampleExecutor;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.querydsl.QuerydslUtils;
import org.springframework.data.repository.core.RepositoryInformation;
@@ -122,4 +124,15 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport {
return Optional.of(new Neo4jQueryLookupStrategy(neo4jOperations, mappingContext, evaluationContextProvider));
}
@Override
protected ProjectionFactory getProjectionFactory() {
ProjectionFactory projectionFactory = super.getProjectionFactory();
if (projectionFactory instanceof SpelAwareProxyProjectionFactory) {
((SpelAwareProxyProjectionFactory) projectionFactory).registerMethodInvokerFactory(
EntityAndGraphPropertyAccessingMethodInterceptor.createMethodInterceptorFactory(mappingContext));
}
return projectionFactory;
}
}

View File

@@ -27,6 +27,8 @@ import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository;
import org.springframework.data.neo4j.repository.query.ReactiveNeo4jQueryLookupStrategy;
import org.springframework.data.neo4j.repository.query.ReactiveQuerydslNeo4jPredicateExecutor;
import org.springframework.data.neo4j.repository.query.SimpleReactiveQueryByExampleExecutor;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.querydsl.QuerydslUtils;
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
import org.springframework.data.repository.core.RepositoryInformation;
@@ -132,4 +134,15 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp
});
}
}
@Override
protected ProjectionFactory getProjectionFactory() {
ProjectionFactory projectionFactory = super.getProjectionFactory();
if (projectionFactory instanceof SpelAwareProxyProjectionFactory) {
((SpelAwareProxyProjectionFactory) projectionFactory).registerMethodInvokerFactory(
EntityAndGraphPropertyAccessingMethodInterceptor.createMethodInterceptorFactory(mappingContext));
}
return projectionFactory;
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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.core.mapping;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.neo4j.integration.shared.common.PersonWithRelationship;
/**
* @author Michael J. Simons
*/
class PropertyFilterTest {
@ParameterizedTest
@CsvSource({ "id, foo", "hobbies, foo", "hobbies.name, hobbies.foo" })
void toDotPathShouldWork(String value, String newDotPath) {
PropertyPath path = PropertyPath.from(value, PersonWithRelationship.class);
String dotPath;
dotPath = PropertyFilter.toDotPath(path, null);
assertThat(dotPath).isEqualTo(path.toDotPath());
dotPath = PropertyFilter.toDotPath(path, "foo");
assertThat(dotPath).isEqualTo(newDotPath);
}
@Nested
class RelaxedPropertyPathTest {
@ParameterizedTest
@ValueSource(strings = { "s1", "s1.s2" })
void toDotPathShouldWork(String value) {
PropertyFilter.RelaxedPropertyPath path = PropertyFilter.RelaxedPropertyPath.withRootType(Object.class)
.append(value);
assertThat(path.toDotPath()).isEqualTo(value);
}
@ParameterizedTest
@CsvSource({ "id, foo", "hobbies, foo", "hobbies.name, hobbies.foo" })
void toDotPathWithReplacementShouldWork(String value, String newDotPath) {
PropertyFilter.RelaxedPropertyPath path = PropertyFilter.RelaxedPropertyPath.withRootType(Object.class)
.append(value);
String dotPath;
dotPath = path.toDotPath(null);
assertThat(dotPath).isEqualTo(path.toDotPath());
dotPath = path.toDotPath("foo");
assertThat(dotPath).isEqualTo(newDotPath);
}
@ParameterizedTest
@CsvSource({ "s1, s1", "s1.s2, s1" })
void getSegmentShouldWork(String value, String segment) {
PropertyFilter.RelaxedPropertyPath path = PropertyFilter.RelaxedPropertyPath.withRootType(Object.class)
.append(value);
assertThat(path.getSegment()).isEqualTo(segment);
}
@ParameterizedTest
@CsvSource({ "s1, s1", "s1.s2, s2", "a.b.c, c" })
void leafSegmentShouldWork(String value, String segment) {
PropertyFilter.RelaxedPropertyPath path = PropertyFilter.RelaxedPropertyPath.withRootType(Object.class)
.append(value);
PropertyFilter.RelaxedPropertyPath leafProperty = path.getLeafProperty();
assertThat(leafProperty.getType()).isEqualTo(path.getType());
assertThat(leafProperty.getSegment()).isEqualTo(segment);
}
}
}

View File

@@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -34,6 +35,7 @@ import org.neo4j.driver.Record;
import org.neo4j.driver.Session;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.Values;
import org.neo4j.driver.types.MapAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
@@ -59,6 +61,7 @@ import org.springframework.data.neo4j.integration.shared.common.Person;
import org.springframework.data.neo4j.integration.shared.common.PersonDepartmentQueryResult;
import org.springframework.data.neo4j.integration.shared.common.PersonEntity;
import org.springframework.data.neo4j.integration.shared.common.PersonSummary;
import org.springframework.data.neo4j.integration.shared.common.PersonWithNoConstructor;
import org.springframework.data.neo4j.integration.shared.common.ProjectionTest1O1;
import org.springframework.data.neo4j.integration.shared.common.ProjectionTestLevel1;
import org.springframework.data.neo4j.integration.shared.common.ProjectionTestRoot;
@@ -107,6 +110,7 @@ class ProjectionIT {
transaction.run("MATCH (n) detach delete n");
transaction.run("CREATE (p:PersonEntity {id: 'p1', email: 'p1@dep1.org'}) -[:MEMBER_OF]->(department:DepartmentEntity {id: 'd1', name: 'Dep1'}) RETURN p");
transaction.run("CREATE (p:PersonWithNoConstructor {name: 'meistermeier', first_name: 'Gerrit', mittlererName: 'unknown'}) RETURN p");
for (Map.Entry<String, String> person : new Map.Entry[] {
new AbstractMap.SimpleEntry(FIRST_NAME, LAST_NAME),
@@ -387,6 +391,38 @@ class ProjectionIT {
assertThat(window.getAdditionalFields()).containsEntry("key1", "value1");
}
@Test // GH-2371
void findWithCustomPropertyNameWorks(@Autowired PersonWithNoConstructorRepository repository) {
assertThat(repository.findAll()).hasSize(1);
ProjectedPersonWithNoConstructor person = repository.findByName("meistermeier");
assertThat(person.getFirstName()).isEqualTo("Gerrit");
assertThat(person.getMittlererName()).isEqualTo("unknown");
}
@Test // GH-2371
void saveWithCustomPropertyNameWorks(@Autowired Neo4jTemplate neo4jTemplate) {
PersonWithNoConstructor person = neo4jTemplate.findOne("MATCH (p:PersonWithNoConstructor {name: 'meistermeier'}) RETURN p", Collections.emptyMap(),
PersonWithNoConstructor.class).get();
person.setName("rotnroll666");
person.setFirstName("Michael");
person.setMiddleName("foo");
neo4jTemplate.saveAs(person, ProjectedPersonWithNoConstructor.class);
try (Session session = driver.session(bookmarkCapture.createSessionConfig())) {
Record record = session
.run("MATCH (p:PersonWithNoConstructor {name: 'rotnroll666'}) RETURN p")
.single();
MapAccessor p = record.get("p").asNode();
assertThat(p.get("first_name").asString()).isEqualTo("Michael");
assertThat(p.get("mittlererName").asString()).isEqualTo("foo");
}
}
private static void projectedEntities(PersonDepartmentQueryResult personAndDepartment) {
assertThat(personAndDepartment.getPerson()).extracting(PersonEntity::getId).isEqualTo("p1");
assertThat(personAndDepartment.getPerson()).extracting(PersonEntity::getEmail).isEqualTo("p1@dep1.org");
@@ -404,6 +440,20 @@ class ProjectionIT {
.build();
}
interface ProjectedPersonWithNoConstructor {
String getName();
String getFirstName();
String getMittlererName();
}
interface PersonWithNoConstructorRepository extends Neo4jRepository<PersonWithNoConstructor, Long> {
ProjectedPersonWithNoConstructor findByName(String name);
}
interface ProjectionPersonRepository extends Neo4jRepository<Person, Long>, CypherdslStatementExecutor<Person> {
Collection<NamesOnly> findByLastName(String lastName);

View File

@@ -17,48 +17,52 @@ package org.springframework.data.neo4j.integration.reactive;
import static org.assertj.core.api.Assertions.assertThat;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.driver.Record;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager;
import org.springframework.data.neo4j.integration.shared.common.ProjectionTest1O1;
import org.springframework.data.neo4j.integration.shared.common.ProjectionTestLevel1;
import org.springframework.data.neo4j.integration.shared.common.ProjectionTestRoot;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.data.neo4j.repository.support.ReactiveCypherdslStatementExecutor;
import org.springframework.data.neo4j.test.BookmarkCapture;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.ReactiveTransactionManager;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Record;
import org.neo4j.driver.Session;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.types.MapAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig;
import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider;
import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager;
import org.springframework.data.neo4j.integration.shared.common.NamesOnly;
import org.springframework.data.neo4j.integration.shared.common.NamesOnlyDto;
import org.springframework.data.neo4j.integration.shared.common.Person;
import org.springframework.data.neo4j.integration.shared.common.PersonSummary;
import org.springframework.data.neo4j.integration.shared.common.PersonWithNoConstructor;
import org.springframework.data.neo4j.integration.shared.common.ProjectionTest1O1;
import org.springframework.data.neo4j.integration.shared.common.ProjectionTestLevel1;
import org.springframework.data.neo4j.integration.shared.common.ProjectionTestRoot;
import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository;
import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.data.neo4j.repository.support.ReactiveCypherdslStatementExecutor;
import org.springframework.data.neo4j.test.BookmarkCapture;
import org.springframework.data.neo4j.test.Neo4jExtension;
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.List;
/**
* @author Gerrit Meier
*/
@@ -90,6 +94,7 @@ class ReactiveProjectionIT {
transaction.run("MATCH (n) detach delete n");
transaction.run("CREATE (:Person{firstName:'%s', lastName:'%s'})-[:LIVES_AT]->(:Address{city:'%s'})" .formatted(FIRST_NAME, LAST_NAME, CITY));
transaction.run("CREATE (p:PersonWithNoConstructor {name: 'meistermeier', first_name: 'Gerrit', mittlererName: 'unknown'}) RETURN p");
Record result = transaction.run("""
create (r:ProjectionTestRoot {name: 'root'})
@@ -303,6 +308,62 @@ class ReactiveProjectionIT {
.verifyComplete();
}
@Test // GH-2371
void findWithCustomPropertyNameWorks(@Autowired PersonWithNoConstructorRepository repository) {
repository.findAll().as(StepVerifier::create).expectNextCount(1L);
repository.findByName("meistermeier")
.as(StepVerifier::create)
.assertNext(person -> {
assertThat(person.getFirstName()).isEqualTo("Gerrit");
assertThat(person.getMittlererName()).isEqualTo("unknown");
})
.verifyComplete();
}
@Test // GH-2371
void saveWithCustomPropertyNameWorks(@Autowired BookmarkCapture bookmarkCapture, @Autowired ReactiveNeo4jTemplate neo4jTemplate) {
neo4jTemplate
.findOne("MATCH (p:PersonWithNoConstructor {name: 'meistermeier'}) RETURN p", Collections.emptyMap(), PersonWithNoConstructor.class)
.doOnNext(person -> {
person.setName("rotnroll666");
person.setFirstName("Michael");
person.setMiddleName("foo");
}).flatMap(p -> neo4jTemplate.saveAs(p, ProjectedPersonWithNoConstructor.class))
.as(StepVerifier::create)
.expectNextCount(1L)
.verifyComplete();
try (Session session = driver.session(bookmarkCapture.createSessionConfig())) {
Record record = session
.run("MATCH (p:PersonWithNoConstructor {name: 'rotnroll666'}) RETURN p")
.single();
MapAccessor p = record.get("p").asNode();
assertThat(p.get("first_name").asString()).isEqualTo("Michael");
assertThat(p.get("mittlererName").asString()).isEqualTo("foo");
}
}
interface ProjectedPersonWithNoConstructor {
String getName();
String getFirstName();
String getMittlererName();
}
interface PersonWithNoConstructorRepository extends ReactiveNeo4jRepository<PersonWithNoConstructor, Long> {
Mono<ProjectedPersonWithNoConstructor> findByName(String name);
}
interface ReactiveProjectionPersonRepository extends ReactiveNeo4jRepository<Person, Long>,
ReactiveCypherdslStatementExecutor<Person> {

View File

@@ -38,4 +38,6 @@ public class PersonWithNoConstructor {
private String name;
@Property("first_name") private String firstName;
@Property("mittlererName") private String middleName;
}