Skip to content

GH-2633 - Be more lenient when mapping DTOs. #2634

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import org.neo4j.driver.Record;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.internal.value.NullValue;
Expand Down Expand Up @@ -82,6 +83,8 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
private final Type relationshipType;
private final Type mapType;
private final Type listType;
private final Type pathType;

private final Map<String, Collection<Node>> labelNodeCache = new HashMap<>();

DefaultNeo4jEntityConverter(EntityInstantiators entityInstantiators, NodeDescriptionStore nodeDescriptionStore,
Expand All @@ -101,6 +104,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
this.relationshipType = typeSystem.RELATIONSHIP();
this.mapType = typeSystem.MAP();
this.listType = typeSystem.LIST();
this.pathType = typeSystem.PATH();
}

@Override
Expand All @@ -112,7 +116,7 @@ public <R> R read(Class<R> targetType, MapAccessor mapAccessor) {

@SuppressWarnings("unchecked") // ¯\_(ツ)_/¯
Neo4jPersistentEntity<R> rootNodeDescription = (Neo4jPersistentEntity<R>) nodeDescriptionStore.getNodeDescription(targetType);
MapAccessor queryRoot = determineQueryRoot(mapAccessor, rootNodeDescription);
MapAccessor queryRoot = determineQueryRoot(mapAccessor, rootNodeDescription, true);

try {
return queryRoot == null ? null : map(queryRoot, queryRoot, rootNodeDescription);
Expand All @@ -122,7 +126,7 @@ public <R> R read(Class<R> targetType, MapAccessor mapAccessor) {
}

@Nullable
private <R> MapAccessor determineQueryRoot(MapAccessor mapAccessor, @Nullable Neo4jPersistentEntity<R> rootNodeDescription) {
private <R> MapAccessor determineQueryRoot(MapAccessor mapAccessor, @Nullable Neo4jPersistentEntity<R> rootNodeDescription, boolean firstTry) {

if (rootNodeDescription == null) {
return null;
Expand Down Expand Up @@ -183,9 +187,40 @@ private <R> MapAccessor determineQueryRoot(MapAccessor mapAccessor, @Nullable Ne
}
}

// The aggregating mapping function synthesizes a bunch of things and we must not interfere with those
boolean isSynthesized = isSynthesized(mapAccessor);
if (!isSynthesized) {
// Check if the original record has been a map. Would have been probably sane to do this right from the start,
// but this would change original SDN 6.0 behaviour to much
if (mapAccessor instanceof Value && ((Value) mapAccessor).hasType(mapType)) {
return mapAccessor;
}

// This is also due the aggregating mapping function: It will check on a NoRootNodeMappingException
// whether there's a nested, aggregatable path
if (firstTry && !canBeAggregated(mapAccessor)) {
Value value = Values.value(Collections.singletonMap("_", mapAccessor.asMap(Function.identity())));
return determineQueryRoot(value, rootNodeDescription, false);
}
}

throw new NoRootNodeMappingException(mapAccessor, rootNodeDescription);
}

private boolean canBeAggregated(MapAccessor mapAccessor) {

if (mapAccessor instanceof Record r) {
return r.values().stream().anyMatch(pathType::isTypeOf);
}
return false;
}

private boolean isSynthesized(MapAccessor mapAccessor) {
return mapAccessor.containsKey(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE) &&
mapAccessor.containsKey(Constants.NAME_OF_SYNTHESIZED_RELATIONS) &&
mapAccessor.containsKey(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES);
}

private Collection<String> createDynamicLabelsProperty(TypeInformation<?> type, Collection<String> dynamicLabels) {

Collection<String> target = CollectionFactory.createCollection(type.getType(), String.class, dynamicLabels.size());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.lite;

/**
* DTO with nested DTO
*
* @author Michael J. Simons
*/
public class A {
private String outer;

private B nested;

public String getOuter() {
return outer;
}

public void setOuter(String outer) {
this.outer = outer;
}

public B getNested() {
return nested;
}

public void setNested(B nested) {
this.nested = nested;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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.lite;

/**
* Inner DTO
*
* @author Michael J. Simons
*/
public class B {
private String inner;

public String getInner() {
return inner;
}

public void setInner(String inner) {
this.inner = inner;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* 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.lite;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Collection;
import java.util.Optional;

import org.junit.jupiter.api.BeforeAll;
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.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;

@Neo4jIntegrationTest
class LightweightMappingIT {

protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;

@BeforeAll
static void setupData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) {

try (Session session = driver.session(bookmarkCapture.createSessionConfig())) {
session.run("MATCH (n) DETACH DELETE n").consume();
// language=cypher
session.run(
"""
CREATE (u1:User {login: 'michael', id: randomUUID()})
CREATE (u2:User {login: 'gerrit', id: randomUUID()})
CREATE (so1:SomeDomainObject {name: 'name1', id: randomUUID()})
CREATE (so2:SomeDomainObject {name: 'name2', id: randomUUID()})
CREATE (so1)<-[:OWNS]-(u1)-[:OWNS]->(so2)
"""
);
bookmarkCapture.seedWith(session.lastBookmarks());
}
}

@Test
void getAllFlatShouldWork(@Autowired SomeDomainRepository repository) {

Collection<MyDTO> dtos = repository.getAllFlat();
assertThat(dtos).hasSize(10)
.allSatisfy(dto -> {
assertThat(dto.counter).isGreaterThan(0);
assertThat(dto.resyncId).isNotNull();
});
}

@Test
void getOneFlatShouldWork(@Autowired SomeDomainRepository repository) {

Optional<MyDTO> dtos = repository.getOneFlat();
assertThat(dtos).hasValueSatisfying(dto -> {
assertThat(dto.counter).isEqualTo(4711L);
assertThat(dto.resyncId).isNotNull();
});
}

@Test
void getAllNestedShouldWork(@Autowired SomeDomainRepository repository) {

Collection<MyDTO> dtos = repository.getNestedStuff();
assertThat(dtos).hasSize(1)
.first()
.satisfies(dto -> {
assertThat(dto.counter).isEqualTo(4711L);
assertThat(dto.resyncId).isNotNull();
assertThat(dto.user)
.isNotNull()
.extracting(User::getLogin)
.isEqualTo("michael");
assertThat(dto.user.getOwnedObjects())
.hasSize(2);

});
}


@Test
void getTestedDTOsShouldWork(@Autowired SomeDomainRepository repository) {

Optional<A> dto = repository.getOneNestedDTO();
assertThat(dto).hasValueSatisfying(v -> {
assertThat(v.getOuter()).isEqualTo("av");
assertThat(v.getNested()).isNotNull()
.extracting(B::getInner).isEqualTo("bv");
});

}

@Configuration
@EnableTransactionManagement
@EnableNeo4jRepositories(considerNestedRepositories = true)
static class Config extends Neo4jImperativeTestConfiguration {

@Bean
public Driver driver() {
return neo4jConnectionSupport.getDriver();
}

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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.lite;

/**
* DTO with optionally linked domain object
*
* @author Michael J. Simons
*/
public class MyDTO {
String resyncId;

Long counter;

User user;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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.lite;

import java.util.UUID;

import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;

/**
* Irrelevant to the tests in this package, but needed for setting up a repository.
*
* @author Michael J. Simons
*/
@Node
public class SomeDomainObject {

@Id
@GeneratedValue
private UUID id;

private final String name;

public SomeDomainObject(String name) {
this.name = name;
}

public UUID getId() {
return id;
}

public String getName() {
return name;
}
}
Loading