Skip to content

Commit adb42ab

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 9178e21 commit adb42ab

File tree

8 files changed

+466
-2
lines changed

8 files changed

+466
-2
lines changed

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

Lines changed: 37 additions & 2 deletions
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;
@@ -82,6 +83,8 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
8283
private final Type relationshipType;
8384
private final Type mapType;
8485
private final Type listType;
86+
private final Type pathType;
87+
8588
private final Map<String, Collection<Node>> labelNodeCache = new HashMap<>();
8689

8790
DefaultNeo4jEntityConverter(EntityInstantiators entityInstantiators, NodeDescriptionStore nodeDescriptionStore,
@@ -101,6 +104,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
101104
this.relationshipType = typeSystem.RELATIONSHIP();
102105
this.mapType = typeSystem.MAP();
103106
this.listType = typeSystem.LIST();
107+
this.pathType = typeSystem.PATH();
104108
}
105109

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

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

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

124128
@Nullable
125-
private <R> MapAccessor determineQueryRoot(MapAccessor mapAccessor, @Nullable Neo4jPersistentEntity<R> rootNodeDescription) {
129+
private <R> MapAccessor determineQueryRoot(MapAccessor mapAccessor, @Nullable Neo4jPersistentEntity<R> rootNodeDescription, boolean firstTry) {
126130

127131
if (rootNodeDescription == null) {
128132
return null;
@@ -183,9 +187,40 @@ private <R> MapAccessor determineQueryRoot(MapAccessor mapAccessor, @Nullable Ne
183187
}
184188
}
185189

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

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

191226
Collection<String> target = CollectionFactory.createCollection(type.getType(), String.class, dynamicLabels.size());
Lines changed: 43 additions & 0 deletions
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+
}
Lines changed: 33 additions & 0 deletions
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+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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.core.DatabaseSelectionProvider;
31+
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
32+
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
33+
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
34+
import org.springframework.data.neo4j.test.BookmarkCapture;
35+
import org.springframework.data.neo4j.test.Neo4jExtension;
36+
import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration;
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+
"""
54+
CREATE (u1:User {login: 'michael', id: randomUUID()})
55+
CREATE (u2:User {login: 'gerrit', id: randomUUID()})
56+
CREATE (so1:SomeDomainObject {name: 'name1', id: randomUUID()})
57+
CREATE (so2:SomeDomainObject {name: 'name2', id: randomUUID()})
58+
CREATE (so1)<-[:OWNS]-(u1)-[:OWNS]->(so2)
59+
"""
60+
);
61+
bookmarkCapture.seedWith(session.lastBookmarks());
62+
}
63+
}
64+
65+
@Test
66+
void getAllFlatShouldWork(@Autowired SomeDomainRepository repository) {
67+
68+
Collection<MyDTO> dtos = repository.getAllFlat();
69+
assertThat(dtos).hasSize(10)
70+
.allSatisfy(dto -> {
71+
assertThat(dto.counter).isGreaterThan(0);
72+
assertThat(dto.resyncId).isNotNull();
73+
});
74+
}
75+
76+
@Test
77+
void getOneFlatShouldWork(@Autowired SomeDomainRepository repository) {
78+
79+
Optional<MyDTO> dtos = repository.getOneFlat();
80+
assertThat(dtos).hasValueSatisfying(dto -> {
81+
assertThat(dto.counter).isEqualTo(4711L);
82+
assertThat(dto.resyncId).isNotNull();
83+
});
84+
}
85+
86+
@Test
87+
void getAllNestedShouldWork(@Autowired SomeDomainRepository repository) {
88+
89+
Collection<MyDTO> dtos = repository.getNestedStuff();
90+
assertThat(dtos).hasSize(1)
91+
.first()
92+
.satisfies(dto -> {
93+
assertThat(dto.counter).isEqualTo(4711L);
94+
assertThat(dto.resyncId).isNotNull();
95+
assertThat(dto.user)
96+
.isNotNull()
97+
.extracting(User::getLogin)
98+
.isEqualTo("michael");
99+
assertThat(dto.user.getOwnedObjects())
100+
.hasSize(2);
101+
102+
});
103+
}
104+
105+
106+
@Test
107+
void getTestedDTOsShouldWork(@Autowired SomeDomainRepository repository) {
108+
109+
Optional<A> dto = repository.getOneNestedDTO();
110+
assertThat(dto).hasValueSatisfying(v -> {
111+
assertThat(v.getOuter()).isEqualTo("av");
112+
assertThat(v.getNested()).isNotNull()
113+
.extracting(B::getInner).isEqualTo("bv");
114+
});
115+
116+
}
117+
118+
@Configuration
119+
@EnableTransactionManagement
120+
@EnableNeo4jRepositories(considerNestedRepositories = true)
121+
static class Config extends Neo4jImperativeTestConfiguration {
122+
123+
@Bean
124+
public Driver driver() {
125+
return neo4jConnectionSupport.getDriver();
126+
}
127+
128+
@Bean
129+
public BookmarkCapture bookmarkCapture() {
130+
return new BookmarkCapture();
131+
}
132+
133+
@Override
134+
public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) {
135+
136+
BookmarkCapture bookmarkCapture = bookmarkCapture();
137+
return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture));
138+
}
139+
140+
@Override
141+
public boolean isCypher5Compatible() {
142+
return neo4jConnectionSupport.isCypher5SyntaxCompatible();
143+
}
144+
}
145+
146+
}
Lines changed: 29 additions & 0 deletions
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+
}
Lines changed: 49 additions & 0 deletions
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)