Search code examples
springkotlinneo4jspring-data-neo4j

spring-data-neo4j v6: No converter found capable of converting from type [MyDTO] to type [org.neo4j.driver.Value]


Situation

I'm migrating a kotlin spring data neo4j application from spring-data-neo4j version 5.2.0.RELEASE to version 6.0.11.

The original application has several Repository interfaces with custom queries which take some DTO as a parameter, and use the various DTO fields to construct the query. All those types of queries currently fail with

org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [MyDTO] to type [org.neo4j.driver.Value]

The reference documentation for spring-data-neo4j v6 only provides examples where parameters passed to custom query methods of a @Repository interface are of the same type as the @Node class associated with that repository. The documentation does not explicitly state that only parameters of the Node class are allowed.

Question

Is there any way to pass an arbitrary DTO (not being a @Node class) to a custom query method in a @Repository interface in spring-data-neo4j v6 like it was possible in v5?

Code samples

Example node entity

@Node
data class MyEntity(
    @Id
    val attr1: String,
    val attr2: String,
    val attr3: String
)

Example DTO

data class MyDTO(
    val field1: String,
    val field2: String
)

Example Repository interface

@Repository
interface MyRepository : PagingAndSortingRepository<MyEntity, String> {

    // ConverterNotFoundException is thrown when this method is called
    @Query("MATCH (e:MyEntity {attr1: {0}.field1}) " +
           "CREATE (e)-[l:LINK]->(n:OtherEntity {attr2: {0}.field2))")
    fun doSomethingWithDto(dto: MyDTO)
}

Solutions tried so far

Annotate DTO as if it were a Node entity

Based on the following found in the reference docs https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#custom-queries.parameters

Mapped entities (everything with a @Node) passed as parameter to a function that is annotated with a custom query will be turned into a nested map.

@Node
data class MyDTO(
    @Id
    val field1: String,
    val field2: String
)

Replace {0} with $0 in custom query

Based on the following found in the reference docs https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#custom-queries.parameters

You do this exactly the same way as in a standard Cypher query issued in the Neo4j Browser or the Cypher-Shell, with the $ syntax (from Neo4j 4.0 on upwards, the old {foo} syntax for Cypher parameters has been removed from the database).

...

[In the given listing] we are referring to the parameter by its name. You can also use $0 etc. instead.

@Repository
interface MyRepository : PagingAndSortingRepository<MyEntity, String> {
    
    // ConverterNotFoundException is thrown when this method is called
    @Query("MATCH (e:MyEntity {attr1: $0.field1}) " +
           "CREATE (e)-[l:LINK]->(n:OtherEntity {attr2: $0.field2))")
    fun doSomethingWithDto(dto: MyDTO)
}

Details

  • spring-boot-starter: v2.4.10
  • spring-data-neo4j: v6.0.12
  • neo4j-java-driver: v4.1.4
  • Neo4j server version: v3.5.29

Solution

  • RTFM Custom conversions ...

    Found the solution myself. Hopefully someone else may benefit from this as well.

    Solution

    Create a custom converter

    import mypackage.model.*
    import com.fasterxml.jackson.core.type.TypeReference
    import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
    import org.neo4j.driver.Value
    import org.neo4j.driver.Values
    import org.springframework.core.convert.TypeDescriptor
    import org.springframework.core.convert.converter.GenericConverter
    import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair
    import java.util.HashSet
    
    class DtoToNeo4jValueConverter : GenericConverter {
        override fun getConvertibleTypes(): Set<ConvertiblePair>? {
            val convertiblePairs: MutableSet<ConvertiblePair> = HashSet()
            convertiblePairs.add(ConvertiblePair(MyDTO::class.java, Value::class.java))
            return convertiblePairs
        }
    
        override fun convert(source: Any?, sourceType: TypeDescriptor, targetType: TypeDescriptor?): Any? {
            return if (MyDTO::class.java.isAssignableFrom(sourceType.type)) {
                // generic way of converting an object into a map
                val dataclassAsMap = jacksonObjectMapper().convertValue(source as MyDTO, object :
                        TypeReference<Map<String, Any>>() {})
                Values.value(dataclassAsMap)
            } else null
        }
    }
    

    Register custom converter in config

    import org.springframework.context.annotation.Bean
    import org.springframework.context.annotation.Configuration
    import org.springframework.data.neo4j.core.convert.Neo4jConversions
    import org.springframework.core.convert.converter.GenericConverter
    import java.util.*
    
    
    @Configuration
    class MyNeo4jConfig {
        @Bean
        override fun neo4jConversions(): Neo4jConversions? {
            val additionalConverters: Set<GenericConverter?> = Collections.singleton(DtoToNeo4jValueConverter())
            return Neo4jConversions(additionalConverters)
        }
    }