fix: Load properties required to instantiate KClasses backing projections proper.

Closes #2899
This commit is contained in:
Michael Simons
2024-05-13 15:38:02 +02:00
parent 2ba67cb71c
commit a471701b4e
4 changed files with 193 additions and 3 deletions

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2011-2024 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;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Predicate;
import org.springframework.core.KotlinDetector;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
import kotlin.reflect.KParameter;
import kotlin.reflect.jvm.ReflectJvmMapping;
/**
* @author Michael J. Simons
*/
final class KPropertyFilterSupport {
/**
* Determines all required constructor args for a Kotlin type
*
* @param type the type for which required constructor args must be determined
* @return a list of property names that need to be fetched
*/
static Collection<String> getRequiredProperties(Class<?> type) {
if (!(KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type))) {
return Collections.emptyList();
}
PreferredConstructor<?, ?> discover = PreferredConstructorDiscoverer.discover(type);
if (discover == null) {
return Collections.emptyList();
}
var preferredConstructor = ReflectJvmMapping.getKotlinFunction(discover.getConstructor());
if (preferredConstructor == null) {
return Collections.emptyList();
}
return preferredConstructor.getParameters().stream()
.filter(Predicate.not(KParameter::isOptional))
.map(KParameter::getName)
.toList();
}
private KPropertyFilterSupport() {
}
}

View File

@@ -45,18 +45,25 @@ public final class PropertyFilterSupport {
Neo4jMappingContext mappingContext) {
ReturnedType returnedType = resultProcessor.getReturnedType();
Class<?> potentiallyProjectedType = returnedType.getReturnedType();
Class<?> domainType = returnedType.getDomainType();
Collection<PropertyFilter.ProjectedPath> filteredProperties = new HashSet<>();
boolean isProjecting = returnedType.isProjecting();
boolean isClosedProjection = factory.getProjectionInformation(returnedType.getReturnedType()).isClosed();
boolean isClosedProjection = factory.getProjectionInformation(potentiallyProjectedType).isClosed();
if (!isProjecting || !isClosedProjection) {
return Collections.emptySet();
}
for (String inputProperty : returnedType.getInputProperties()) {
addPropertiesFrom(returnedType.getDomainType(), returnedType.getReturnedType(), factory,
filteredProperties, new ProjectionPathProcessor(inputProperty, PropertyPath.from(inputProperty, returnedType.getReturnedType()).getLeafProperty().getTypeInformation()), mappingContext);
addPropertiesFrom(domainType, potentiallyProjectedType, factory,
filteredProperties, new ProjectionPathProcessor(inputProperty, PropertyPath.from(inputProperty, potentiallyProjectedType).getLeafProperty().getTypeInformation()), mappingContext);
}
for (String inputProperty : KPropertyFilterSupport.getRequiredProperties(domainType)) {
addPropertiesFrom(domainType, potentiallyProjectedType, factory,
filteredProperties, new ProjectionPathProcessor(inputProperty, PropertyPath.from(inputProperty, domainType).getLeafProperty().getTypeInformation()), mappingContext);
}
return filteredProperties;

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2011-2024 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.k
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.neo4j.driver.Driver
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.neo4j.config.AbstractNeo4jConfig
import org.springframework.data.neo4j.core.DatabaseSelectionProvider
import org.springframework.data.neo4j.core.Neo4jTemplate
import org.springframework.data.neo4j.core.schema.Id
import org.springframework.data.neo4j.core.schema.Node
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager
import org.springframework.data.neo4j.repository.Neo4jRepository
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.Neo4jIntegrationTest
import org.springframework.stereotype.Repository
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.annotation.EnableTransactionManagement
import org.springframework.transaction.support.TransactionTemplate
import java.util.*
/**
* @author Michael J. Simons
*/
@Neo4jIntegrationTest
internal class KotlinIssuesIT {
companion object {
@JvmStatic
private lateinit var neo4jConnectionSupport: Neo4jExtension.Neo4jConnectionSupport
@BeforeAll
@JvmStatic
fun clearDatabase(@Autowired driver: Driver, @Autowired bookmarkCapture: BookmarkCapture) {
driver.session().use { session ->
session.run("MATCH (n) DETACH DELETE n").consume()
bookmarkCapture.seedWith(session.lastBookmarks())
}
}
}
@Test // GH-2899
fun requiredPropertiesMustBeIncludedInProjections(@Autowired someRepository: KotlinDataClassEntityRepository) {
someRepository.save(KotlinDataClassEntity(propertyOne = "one", propertyTwo = "two"))
val p = someRepository.findAllProjectedBy()
assertThat(p).hasSizeGreaterThan(0)
.first().matches { v -> v.propertyOne == "one" }
}
@Node
data class KotlinDataClassEntity (
@Id
val id: String = UUID.randomUUID().toString(),
val propertyOne: String,
val propertyTwo: String
)
interface KotlinDataClassEntityProjection {
val propertyOne: String
}
@Repository
internal interface KotlinDataClassEntityRepository : Neo4jRepository<KotlinDataClassEntity, String> {
fun findAllProjectedBy(): List<KotlinDataClassEntityProjection>
}
@Configuration
@EnableTransactionManagement
@EnableNeo4jRepositories(considerNestedRepositories = true)
open class MyConfig : AbstractNeo4jConfig() {
@Bean
override fun driver(): Driver {
return neo4jConnectionSupport.driver
}
@Bean
open fun bookmarkCapture(): BookmarkCapture {
return BookmarkCapture()
}
@Bean
override fun transactionManager(driver: Driver, databaseNameProvider: DatabaseSelectionProvider): PlatformTransactionManager {
val bookmarkCapture = bookmarkCapture()
return Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture))
}
@Bean
open fun transactionTemplate(transactionManager: PlatformTransactionManager): TransactionTemplate {
return TransactionTemplate(transactionManager)
}
}
}

View File

@@ -0,0 +1,5 @@
/**
* Add `k` as package name as I couldn't figure out in which way the presence of KotlinIssuesIT in the split package
* (between the Kotlin and Java code) messed up the application context in tests.
*/
package org.springframework.data.neo4j.integration.k;