fix: Load properties required to instantiate KClasses backing projections proper.
Closes #2899
This commit is contained in:
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user