Skip to content

Commit 8c297fd

Browse files
GH-2262 - Fetch all properties when querying via a base interface.
When a statement is generated for a node description that describes an interface, we now always add the `__allProperties__` attribute to the map of properties pointing to `*`. We must do this because we cannot assume that every implementation of the interface actually has only the properties of the interface, but an arbitrary set of unknowns. The converter will now always look for this property if a requested property is not found on the surrounding map accessor. This fixes #2262.
1 parent 1a6d18a commit 8c297fd

File tree

5 files changed

+170
-2
lines changed

5 files changed

+170
-2
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ private List<Object> projectNodeProperties(NodeDescription<?> nodeDescription, S
559559
nodePropertiesProjection.add(graphProperty.getPropertyName());
560560
}
561561

562-
if (hasCompositeProperties) {
562+
if (hasCompositeProperties || nodeDescription.describesInterface()) {
563563
nodePropertiesProjection.add(Constants.NAME_OF_ALL_PROPERTIES);
564564
nodePropertiesProjection.add(node.project(Cypher.asterisk()));
565565
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
import org.neo4j.driver.Value;
3737
import org.neo4j.driver.Values;
38+
import org.neo4j.driver.internal.value.NullValue;
3839
import org.neo4j.driver.types.Entity;
3940
import org.neo4j.driver.types.MapAccessor;
4041
import org.neo4j.driver.types.Node;
@@ -559,7 +560,13 @@ private static Value extractValueOf(Neo4jPersistentProperty property, MapAccesso
559560
}
560561
} else {
561562
String graphPropertyName = property.getPropertyName();
562-
return propertyContainer.get(graphPropertyName);
563+
if (propertyContainer.containsKey(graphPropertyName)) {
564+
return propertyContainer.get(graphPropertyName);
565+
} else if (propertyContainer.containsKey(Constants.NAME_OF_ALL_PROPERTIES)) {
566+
return propertyContainer.get(Constants.NAME_OF_ALL_PROPERTIES).get(graphPropertyName);
567+
} else {
568+
return NullValue.NULL;
569+
}
563570
}
564571
}
565572

src/test/java/org/springframework/data/neo4j/integration/imperative/InheritanceMappingIT.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import org.springframework.data.neo4j.integration.shared.common.Cat;
3232
import org.springframework.data.neo4j.integration.shared.common.Dog;
3333
import org.springframework.data.neo4j.integration.shared.common.Inheritance;
34+
import org.springframework.data.neo4j.integration.shared.common.KotlinAnimationMovie;
35+
import org.springframework.data.neo4j.integration.shared.common.KotlinCinema;
3436
import org.springframework.data.neo4j.repository.Neo4jRepository;
3537
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
3638
import org.springframework.data.neo4j.repository.query.Query;
@@ -366,6 +368,25 @@ void shouldMatchPolymorphicInterfacesWhenFetchingAll(@Autowired ParentModelRepos
366368
assertThat(divisions).first().satisfies(twoDifferentInterfacesHaveBeenLoaded());
367369
}
368370

371+
@Test // GH-2262
372+
void shouldMatchPolymorphicKotlinInterfacesWhenFetchingAll(@Autowired CinemaRepository repository) {
373+
374+
try (Session session = driver.session(); Transaction transaction = session.beginTransaction()) {
375+
transaction.run("CREATE (:KotlinMovie:KotlinAnimationMovie {id: 'movie001', name: 'movie-001', studio: 'Pixar'})<-[:Plays]-(c:KotlinCinema {id:'cine-01', name: 'GrandRex'}) RETURN id(c) AS id")
376+
.single().get(0).asLong();
377+
transaction.commit();
378+
}
379+
380+
List<KotlinCinema> divisions = repository.findAll();
381+
assertThat(divisions).hasSize(1);
382+
assertThat(divisions).first().satisfies(c -> {
383+
assertThat(c.getPlays()).hasSize(1);
384+
assertThat(c.getPlays()).first().isInstanceOf(KotlinAnimationMovie.class)
385+
.extracting(m -> ((KotlinAnimationMovie) m).getStudio())
386+
.isEqualTo("Pixar");
387+
});
388+
}
389+
369390
private Consumer<Inheritance.ParentModel2> twoDifferentInterfacesHaveBeenLoaded() {
370391
return d -> {
371392
assertThat(d.getIsRelatedTo()).hasSize(2);
@@ -421,6 +442,8 @@ interface DivisionRepository extends Neo4jRepository<Inheritance.Division, Long>
421442

422443
interface ParentModelRepository extends Neo4jRepository<Inheritance.ParentModel2, Long> {}
423444

445+
interface CinemaRepository extends Neo4jRepository<KotlinCinema, String> {}
446+
424447
@Configuration
425448
@EnableNeo4jRepositories(considerNestedRepositories = true)
426449
@EnableTransactionManagement
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2011-2021 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+
17+
package org.springframework.data.neo4j.integration.imperative
18+
19+
import org.assertj.core.api.Assertions.assertThat
20+
import org.junit.jupiter.api.BeforeAll
21+
import org.junit.jupiter.api.Test
22+
import org.neo4j.driver.Driver
23+
import org.springframework.beans.factory.annotation.Autowired
24+
import org.springframework.context.annotation.Bean
25+
import org.springframework.context.annotation.Configuration
26+
import org.springframework.data.neo4j.config.AbstractNeo4jConfig
27+
import org.springframework.data.neo4j.core.Neo4jTemplate
28+
import org.springframework.data.neo4j.integration.shared.common.Inheritance
29+
import org.springframework.data.neo4j.integration.shared.common.KotlinAnimationMovie
30+
import org.springframework.data.neo4j.integration.shared.common.KotlinCinema
31+
import org.springframework.data.neo4j.repository.Neo4jRepository
32+
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories
33+
import org.springframework.data.neo4j.test.Neo4jExtension
34+
import org.springframework.data.neo4j.test.Neo4jIntegrationTest
35+
import org.springframework.transaction.PlatformTransactionManager
36+
import org.springframework.transaction.annotation.EnableTransactionManagement
37+
import org.springframework.transaction.support.TransactionTemplate
38+
39+
/**
40+
* @author Michael J. Simons
41+
* @soundtrack Genesis - Invisible Touch
42+
*/
43+
@Neo4jIntegrationTest
44+
class KotlinInheritanceIT @Autowired constructor(
45+
private val template: Neo4jTemplate,
46+
private val driver: Driver,
47+
private val transactionTemplate: TransactionTemplate
48+
) {
49+
50+
companion object {
51+
@JvmStatic
52+
private lateinit var neo4jConnectionSupport: Neo4jExtension.Neo4jConnectionSupport
53+
54+
@BeforeAll
55+
@JvmStatic
56+
fun clearDatabase(@Autowired driver: Driver) {
57+
driver.session().use { session ->
58+
session.run("MATCH (n) DETACH DELETE n").consume()
59+
}
60+
}
61+
}
62+
63+
@Test // GH-2262
64+
fun shouldMatchPolymorphicInterfacesWhenFetchingAll(@Autowired cinemaRepository: KotlinCinemaRepository) {
65+
66+
driver.session().use { session ->
67+
session.writeTransaction { tx ->
68+
tx.run("CREATE (:KotlinMovie:KotlinAnimationMovie {id: 'movie001', name: 'movie-001', studio: 'Pixar'})<-[:Plays]-(c:KotlinCinema {id:'cine-01', name: 'GrandRex'}) RETURN id(c) AS id")
69+
.single()["id"].asLong()
70+
}
71+
}
72+
73+
val cinemas = cinemaRepository.findAll()
74+
assertThat(cinemas).hasSize(1);
75+
assertThat(cinemas).first().satisfies { c ->
76+
assertThat(c.plays).hasSize(1);
77+
assertThat(c.plays).first().isInstanceOf(KotlinAnimationMovie::class.java)
78+
.extracting { m -> (m as KotlinAnimationMovie).studio }
79+
.isEqualTo("Pixar")
80+
}
81+
}
82+
83+
interface KotlinCinemaRepository : Neo4jRepository<KotlinCinema, String>
84+
85+
@Configuration
86+
@EnableTransactionManagement
87+
@EnableNeo4jRepositories(considerNestedRepositories = true)
88+
open class MyConfig : AbstractNeo4jConfig() {
89+
@Bean
90+
override fun driver(): Driver {
91+
return neo4jConnectionSupport.driver
92+
}
93+
94+
override fun getMappingBasePackages(): Collection<String?>? {
95+
return setOf(Inheritance::class.java.getPackage().name)
96+
}
97+
98+
@Bean
99+
open fun transactionTemplate(transactionManager: PlatformTransactionManager): TransactionTemplate {
100+
return TransactionTemplate(transactionManager)
101+
}
102+
}
103+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2011-2021 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.shared.common
17+
18+
import org.springframework.data.neo4j.core.schema.Id
19+
import org.springframework.data.neo4j.core.schema.Node
20+
import org.springframework.data.neo4j.core.schema.Relationship
21+
22+
@Node
23+
interface KotlinMovie {
24+
val id: String
25+
val name: String
26+
}
27+
28+
29+
@Node
30+
class KotlinCinema(@Id val id: String, val name: String,
31+
@Relationship("Plays", direction = Relationship.Direction.OUTGOING) val plays: List<KotlinMovie>
32+
)
33+
34+
@Node
35+
class KotlinAnimationMovie(@Id override val id: String, override val name: String, val studio: String?) : KotlinMovie

0 commit comments

Comments
 (0)