Skip to content

Commit 961bf87

Browse files
GH-2633 - Be more lenient when mapping DTOs. (#2634)
We can support many more usecases of "I want to have a lightweight mapping tool" for DTOs by just restricting the cases in which a `NoRootNodeMappingException` is thrown. The idea is as follows: If the `DefaultNeo4jEntityConverter` does not deal with a synthesized record comming from `AggregatingMappingFunction` it uses the the root map as base for mapping if the root record is already a map value. If that is not the case and the root record does not contain any paths that maybe aggregated later one, we synthesize a map one single time and evaluate that for being mappable. Closes #2633.
1 parent 2c6870e commit 961bf87

File tree

8 files changed

+458
-2
lines changed

8 files changed

+458
-2
lines changed

src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java

+38-2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.stream.Collectors;
3535
import java.util.stream.StreamSupport;
3636

37+
import org.neo4j.driver.Record;
3738
import org.neo4j.driver.Value;
3839
import org.neo4j.driver.Values;
3940
import org.neo4j.driver.internal.value.NullValue;
@@ -79,6 +80,8 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
7980
private final Type relationshipType;
8081
private final Type mapType;
8182
private final Type listType;
83+
private final Type pathType;
84+
8285
private final Map<String, Collection<Node>> labelNodeCache = new HashMap<>();
8386

8487
DefaultNeo4jEntityConverter(EntityInstantiators entityInstantiators, Neo4jConversionService conversionService,
@@ -97,6 +100,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
97100
this.relationshipType = typeSystem.RELATIONSHIP();
98101
this.mapType = typeSystem.MAP();
99102
this.listType = typeSystem.LIST();
103+
this.pathType = typeSystem.PATH();
100104
}
101105

102106
@Override
@@ -108,7 +112,7 @@ public <R> R read(Class<R> targetType, MapAccessor mapAccessor) {
108112

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

113117
try {
114118
return queryRoot == null ? null : map(queryRoot, queryRoot, rootNodeDescription);
@@ -118,7 +122,7 @@ public <R> R read(Class<R> targetType, MapAccessor mapAccessor) {
118122
}
119123

120124
@Nullable
121-
private <R> MapAccessor determineQueryRoot(MapAccessor mapAccessor, @Nullable Neo4jPersistentEntity<R> rootNodeDescription) {
125+
private <R> MapAccessor determineQueryRoot(MapAccessor mapAccessor, @Nullable Neo4jPersistentEntity<R> rootNodeDescription, boolean firstTry) {
122126

123127
if (rootNodeDescription == null) {
124128
return null;
@@ -179,9 +183,41 @@ private <R> MapAccessor determineQueryRoot(MapAccessor mapAccessor, @Nullable Ne
179183
}
180184
}
181185

186+
// The aggregating mapping function synthesizes a bunch of things and we must not interfere with those
187+
boolean isSynthesized = isSynthesized(mapAccessor);
188+
if (!isSynthesized) {
189+
// Check if the original record has been a map. Would have been probably sane to do this right from the start,
190+
// but this would change original SDN 6.0 behaviour to much
191+
if (mapAccessor instanceof Value && ((Value) mapAccessor).hasType(mapType)) {
192+
return mapAccessor;
193+
}
194+
195+
// This is also due the aggregating mapping function: It will check on a NoRootNodeMappingException
196+
// whether there's a nested, aggregatable path
197+
if (firstTry && !canBeAggregated(mapAccessor)) {
198+
Value value = Values.value(Collections.singletonMap("_", mapAccessor.asMap(Function.identity())));
199+
return determineQueryRoot(value, rootNodeDescription, false);
200+
}
201+
}
202+
182203
throw new NoRootNodeMappingException(mapAccessor, rootNodeDescription);
183204
}
184205

206+
private boolean canBeAggregated(MapAccessor mapAccessor) {
207+
208+
if (mapAccessor instanceof Record) {
209+
Record r = ((Record) mapAccessor);
210+
return r.values().stream().anyMatch(pathType::isTypeOf);
211+
}
212+
return false;
213+
}
214+
215+
private boolean isSynthesized(MapAccessor mapAccessor) {
216+
return mapAccessor.containsKey(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE) &&
217+
mapAccessor.containsKey(Constants.NAME_OF_SYNTHESIZED_RELATIONS) &&
218+
mapAccessor.containsKey(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES);
219+
}
220+
185221
private Collection<String> createDynamicLabelsProperty(TypeInformation<?> type, Collection<String> dynamicLabels) {
186222

187223
Collection<String> target = CollectionFactory.createCollection(type.getType(), String.class, dynamicLabels.size());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2011-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.lite;
17+
18+
/**
19+
* DTO with nested DTO
20+
*
21+
* @author Michael J. Simons
22+
*/
23+
public class A {
24+
private String outer;
25+
26+
private B nested;
27+
28+
public String getOuter() {
29+
return outer;
30+
}
31+
32+
public void setOuter(String outer) {
33+
this.outer = outer;
34+
}
35+
36+
public B getNested() {
37+
return nested;
38+
}
39+
40+
public void setNested(B nested) {
41+
this.nested = nested;
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2011-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.lite;
17+
18+
/**
19+
* Inner DTO
20+
*
21+
* @author Michael J. Simons
22+
*/
23+
public class B {
24+
private String inner;
25+
26+
public String getInner() {
27+
return inner;
28+
}
29+
30+
public void setInner(String inner) {
31+
this.inner = inner;
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright 2011-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.lite;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.Collection;
21+
import java.util.Optional;
22+
23+
import org.junit.jupiter.api.BeforeAll;
24+
import org.junit.jupiter.api.Test;
25+
import org.neo4j.driver.Driver;
26+
import org.neo4j.driver.Session;
27+
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
31+
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
32+
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
33+
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
34+
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
35+
import org.springframework.data.neo4j.test.BookmarkCapture;
36+
import org.springframework.data.neo4j.test.Neo4jExtension;
37+
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
38+
import org.springframework.transaction.PlatformTransactionManager;
39+
import org.springframework.transaction.annotation.EnableTransactionManagement;
40+
41+
@Neo4jIntegrationTest
42+
class LightweightMappingIT {
43+
44+
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
45+
46+
@BeforeAll
47+
static void setupData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) {
48+
49+
try (Session session = driver.session(bookmarkCapture.createSessionConfig())) {
50+
session.run("MATCH (n) DETACH DELETE n").consume();
51+
// language=cypher
52+
session.run(
53+
"CREATE (u1:User {login: 'michael', id: randomUUID()})\n" +
54+
"CREATE (u2:User {login: 'gerrit', id: randomUUID()})\n" +
55+
"CREATE (so1:SomeDomainObject {name: 'name1', id: randomUUID()})\n" +
56+
"CREATE (so2:SomeDomainObject {name: 'name2', id: randomUUID()})\n" +
57+
"CREATE (so1)<-[:OWNS]-(u1)-[:OWNS]->(so2)\n"
58+
);
59+
bookmarkCapture.seedWith(session.lastBookmark());
60+
}
61+
}
62+
63+
@Test
64+
void getAllFlatShouldWork(@Autowired SomeDomainRepository repository) {
65+
66+
Collection<MyDTO> dtos = repository.getAllFlat();
67+
assertThat(dtos).hasSize(10)
68+
.allSatisfy(dto -> {
69+
assertThat(dto.counter).isGreaterThan(0);
70+
assertThat(dto.resyncId).isNotNull();
71+
});
72+
}
73+
74+
@Test
75+
void getOneFlatShouldWork(@Autowired SomeDomainRepository repository) {
76+
77+
Optional<MyDTO> dtos = repository.getOneFlat();
78+
assertThat(dtos).hasValueSatisfying(dto -> {
79+
assertThat(dto.counter).isEqualTo(4711L);
80+
assertThat(dto.resyncId).isNotNull();
81+
});
82+
}
83+
84+
@Test
85+
void getAllNestedShouldWork(@Autowired SomeDomainRepository repository) {
86+
87+
Collection<MyDTO> dtos = repository.getNestedStuff();
88+
assertThat(dtos).hasSize(1)
89+
.first()
90+
.satisfies(dto -> {
91+
assertThat(dto.counter).isEqualTo(4711L);
92+
assertThat(dto.resyncId).isNotNull();
93+
assertThat(dto.user)
94+
.isNotNull()
95+
.extracting(User::getLogin)
96+
.isEqualTo("michael");
97+
assertThat(dto.user.getOwnedObjects())
98+
.hasSize(2);
99+
100+
});
101+
}
102+
103+
104+
@Test
105+
void getTestedDTOsShouldWork(@Autowired SomeDomainRepository repository) {
106+
107+
Optional<A> dto = repository.getOneNestedDTO();
108+
assertThat(dto).hasValueSatisfying(v -> {
109+
assertThat(v.getOuter()).isEqualTo("av");
110+
assertThat(v.getNested()).isNotNull()
111+
.extracting(B::getInner).isEqualTo("bv");
112+
});
113+
114+
}
115+
116+
@Configuration
117+
@EnableTransactionManagement
118+
@EnableNeo4jRepositories(considerNestedRepositories = true)
119+
static class Config extends AbstractNeo4jConfig {
120+
121+
@Bean
122+
public Driver driver() {
123+
return neo4jConnectionSupport.getDriver();
124+
}
125+
126+
@Bean
127+
public BookmarkCapture bookmarkCapture() {
128+
return new BookmarkCapture();
129+
}
130+
131+
@Override
132+
public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) {
133+
134+
BookmarkCapture bookmarkCapture = bookmarkCapture();
135+
return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture));
136+
}
137+
}
138+
139+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2011-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.lite;
17+
18+
/**
19+
* DTO with optionally linked domain object
20+
*
21+
* @author Michael J. Simons
22+
*/
23+
public class MyDTO {
24+
String resyncId;
25+
26+
Long counter;
27+
28+
User user;
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2011-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.lite;
17+
18+
import java.util.UUID;
19+
20+
import org.springframework.data.neo4j.core.schema.GeneratedValue;
21+
import org.springframework.data.neo4j.core.schema.Id;
22+
import org.springframework.data.neo4j.core.schema.Node;
23+
24+
/**
25+
* Irrelevant to the tests in this package, but needed for setting up a repository.
26+
*
27+
* @author Michael J. Simons
28+
*/
29+
@Node
30+
public class SomeDomainObject {
31+
32+
@Id
33+
@GeneratedValue
34+
private UUID id;
35+
36+
private final String name;
37+
38+
public SomeDomainObject(String name) {
39+
this.name = name;
40+
}
41+
42+
public UUID getId() {
43+
return id;
44+
}
45+
46+
public String getName() {
47+
return name;
48+
}
49+
}

0 commit comments

Comments
 (0)