Spring Data Neo4j with Kotlin relation example

So we are using
Spring Boot 2.4.2 and a desktop project of 4.2.3

The application ...

@SpringBootApplication
@EnableNeo4jRepositories
@Transactional
class DemoApplication{
    @Bean
    fun init (kennelRepository: KennelRepository, dogRepository:DogRepository): CommandLineRunner {
        return CommandLineRunner {
            kennelRepository.deleteAll()
            dogRepository.deleteAll()
            var dogFido: Dog = Dog(name = "fido")
            dogRepository.save<Dog>(dogFido)
            var dogRover: Dog = Dog(name = "rover")
            var kennelForRover: Kennel = Kennel(dog = dogRover)
            kennelRepository.save<Kennel>(kennelForRover)
            print("complete")
        }
    }
}

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

the entities ...

@Node
data class Dog constructor(@Id @GeneratedValue val id: Long? = null, val name: String){
    fun withId(id: Long): Dog {
        return if (this.id == id) {
            this
        } else {
            this.copy(id = id)
        }
    }
}
@Node
data class Kennel constructor (@Id @GeneratedValue val id: Long? = null, @Relationship val dog: Dog) {
    fun withId(id: Long): Kennel {
        return if (this.id == id) {
            this
        } else {
            this.copy(id = id)
        }
    }
}

the repositories

@Repository
interface DogRepository : Neo4jRepository<Dog, Long>
@Repository
interface KennelRepository : Neo4jRepository<Kennel, Long>

So, the dog repository save of fido seems to work fine, but the kennel repository save gives an error ...

Caused by: java.lang.NullPointerException: null
	at org.springframework.data.neo4j.core.Neo4jTemplate.convertIdValues(Neo4jTemplate.java:218) ~[spring-data-neo4j-6.0.3.jar:6.0.3]
	at org.springframework.data.neo4j.core.Neo4jTemplate.lambda$processNestedRelations$16(Neo4jTemplate.java:506) ~[spring-data-neo4j-6.0.3.jar:6.0.3]
	at org.springframework.data.mapping.model.BasicPersistentEntity.doWithAssociations(BasicPersistentEntity.java:387) ~[spring-data-commons-2.4.3.jar:2.4.3]
	at org.springframework.data.neo4j.core.Neo4jTemplate.processNestedRelations(Neo4jTemplate.java:452) ~[spring-data-neo4j-6.0.3.jar:6.0.3]
	at org.springframework.data.neo4j.core.Neo4jTemplate.processRelations(Neo4jTemplate.java:442) ~[spring-data-neo4j-6.0.3.jar:6.0.3]
	at org.springframework.data.neo4j.core.Neo4jTemplate.saveImpl(Neo4jTemplate.java:254) ~[spring-data-neo4j-6.0.3.jar:6.0.3]
	at org.springframework.data.neo4j.core.Neo4jTemplate.save(Neo4jTemplate.java:225) ~[spring-data-neo4j-6.0.3.jar:6.0.3]
	at org.springframework.data.neo4j.repository.support.SimpleNeo4jRepository.save(SimpleNeo4jRepository.java:131) ~[spring-data-neo4j-6.0.3.jar:6.0.3]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:289) ~[spring-data-commons-2.4.3.jar:2.4.3]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137) ~[spring-data-commons-2.4.3.jar:2.4.3]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121) ~[spring-data-commons-2.4.3.jar:2.4.3]
	at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:524) ~[spring-data-commons-2.4.3.jar:2.4.3]
	at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285) ~[spring-data-commons-2.4.3.jar:2.4.3]
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:531) ~[spring-data-commons-2.4.3.jar:2.4.3]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.3.jar:5.3.3]
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:156) ~[spring-data-commons-2.4.3.jar:2.4.3]
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:131) ~[spring-data-commons-2.4.3.jar:2.4.3]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.3.jar:5.3.3]
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80) ~[spring-data-commons-2.4.3.jar:2.4.3]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.3.jar:5.3.3]
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.3.jar:5.3.3]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.3.jar:5.3.3]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.3.jar:5.3.3]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.3.jar:5.3.3]
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137) ~[spring-tx-5.3.3.jar:5.3.3]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.3.jar:5.3.3]
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-5.3.3.jar:5.3.3]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.3.jar:5.3.3]
	at org.springframework.data.repository.core.support.MethodInvocationValidator.invoke(MethodInvocationValidator.java:98) ~[spring-data-commons-2.4.3.jar:2.4.3]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.3.jar:5.3.3]
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) ~[spring-aop-5.3.3.jar:5.3.3]
	at com.sun.proxy.$Proxy69.save(Unknown Source) ~[na:na]
	at com.example.demo.DemoApplication$init$1.run(DemoApplication.kt:27) ~[main/:na]
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:804) ~[spring-boot-2.4.2.jar:2.4.2]
	... 5 common frames omitted

I have written the equivalent code in Java and it all seems to work fine.
So I feel that I am missing something fairly fundamental here.

Any thoughts are very welcome.

This is related to NullPointerException when persisting Kotlin data class · Issue #2141 · spring-projects/spring-data-neo4j · GitHub

@gerrit.meier
Hello my friend, is your patch in 6.1.0-M3 ?

The bug fix is also included in the 6.0.4.

Thanks @gerrit.meier
I replaced the

implementation("org.springframework.boot:spring-boot-starter-data-neo4j")

with the

implementation("org.springframework.data:spring-data-neo4j:6.0.4")

I can confirm that 6.0.4 no longer gives a NullPointerException
However, I am now receiving an exception

java.lang.IllegalStateException: Failed to execute CommandLineRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:807) ~[spring-boot-2.4.2.jar:2.4.2]
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:788) ~[spring-boot-2.4.2.jar:2.4.2]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:333) ~[spring-boot-2.4.2.jar:2.4.2]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1311) ~[spring-boot-2.4.2.jar:2.4.2]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300) ~[spring-boot-2.4.2.jar:2.4.2]
	at com.example.demo.DemoApplicationKt.main(DemoApplication.kt:40) ~[main/:na]
Caused by: org.springframework.dao.InvalidDataAccessResourceUsageException: Exactly one relationship type must be specified for MERGE. Did you forget to prefix your relationship type with a ':'? (line 1, column 108 (offset: 107))
"MATCH (startNode) WHERE id(startNode) = $fromId MATCH (endNode) WHERE id(endNode) = $toId MERGE (startNode)-[relProps]->(endNode) RETURN id(relProps)"
                                                                                                            ^; Error code 'Neo.ClientError.Statement.SyntaxError'
	at org.springframework.data.neo4j.core.Neo4jPersistenceExceptionTranslator.translateImpl(Neo4jPersistenceExceptionTranslator.java:105) ~[spring-data-neo4j-6.0.4.jar:6.0.4]
	at org.springframework.data.neo4j.core.Neo4jPersistenceExceptionTranslator.translateExceptionIfPossible(Neo4jPersistenceExceptionTranslator.java:91) ~[spring-data-neo4j-6.0.4.jar:6.0.4]
	at org.springframework.data.neo4j.core.DefaultNeo4jClient.potentiallyConvertRuntimeException(DefaultNeo4jClient.java:174) ~[spring-data-neo4j-6.0.4.jar:6.0.4]
	at org.springframework.data.neo4j.core.DefaultNeo4jClient.access$400(DefaultNeo4jClient.java:55) ~[spring-data-neo4j-6.0.4.jar:6.0.4]
	at org.springframework.data.neo4j.core.DefaultNeo4jClient$DefaultRecordFetchSpec.one(DefaultNeo4jClient.java:289) ~[spring-data-neo4j-6.0.4.jar:6.0.4]

when I try to save kennelForRover.
Once again, as before, dogFido was able to be saved.

I appreciate that with your efforts on Return related objects with ids set after save · Issue #2148 · spring-projects/spring-data-neo4j · GitHub, this whole area is a work in progress.

Still, this feels like a separate concern.
Indeed, I strongly suspect that I have just missed something obvious.

Please let me know if you want me to add this matter to Issue 2148 for reference or whether I should fire up a separate Github issue.

Many thanks.

Thanks for your very quick feedback. :bowing_man:
You are defining the @Relationship without any type. The type will default to an empty String in this case.
Leaving the @Relationship definition completely out to generate the type from the parameter name is one option (but not recommended).
Providing the type explicitly is the better option: @Relationship("IS_HOME_OF")

And thanks for your rapid feedback.
Yes typing the relationship worked wonders.
Thank you for your help in giving a dog a home. :grinning:

1 Like