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

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

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)
}
}

It's ridiculous that the framework would force you to write a custom converter for this. I made a #Transient object in my overridden User class for a limited set of update-able user profile fields, and I'm encountering the same error. I guess I will just have to break up the object into its component String fields in the method params to get around this problem. What a mess.
#Query("MATCH (u:User) WHERE u.username = :#{#username} SET u.firstName = :#{#up.firstName},u.lastName = :#{#up.firstName},u.intro = :#{#up.intro} RETURN u")
Mono<User> update(#Param("username") String username,#Param("up") UserProfile up);
No converter found capable of converting from type [...UserProfile] to type [org.neo4j.driver.Value]

Related

GraphQL mutation implementation in Spring Boot using #GraphQlRepository

I have the following Spring Boot reactive "stack" with GraphQL and MongoDB (in Kotlin):
spring-boot-starter-webflux
spring-boot-starter-graphql
spring-boot-starter-data-mongodb-reactive
A very basic example for a server which exposes a GraphQL API to query customers:
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.graphql.data.GraphQlRepository
#SpringBootApplication class ServerApplication
fun main(args: Array<String>) {
runApplication<ServerApplication>(*args)
}
#Document
data class Customer(
#Id val id: String? = null,
val name: String?,
)
#GraphQlRepository
interface CustomerRepository : ReactiveMongoRepository<Customer, String>
In combination with the following GraphQL schema file
type Customer {
id: ID
name: String
}
type Query {
customers: [Customer]
customerById(id: ID!): Customer
}
type Mutation {
createCustomer(name: String!): Customer
}
It is already possible to query customers / customerById and retrieve the data accordingly using e.g.:
{
customers { id name }
customerById(id: "...") { name }
}
This is made possible by the #GraphQlRepository annotation, which automatically registers a handler for fetching data directly from the database.
However, I can't find anything in the documentation about how mutations are implemented i.e. if there is such a simple automatic solution like for the queries or if this has to be implemented manually by a controller with #MutationMapping.
#Controller
class CustomerController(val customerRepository: CustomerRepository) {
#MutationMapping
fun createCustomer(#Argument name: String?): Mono<Customer> {
return customerRepository.save(Customer(name = name))
}
}
As far as I know, mutations must be implemented through a dedicated, #MutationMapping annotated method in the Controller like you suggest. I have made a couple of exercises and the only difference from your example is that I have used a special Input type -both in the schema and in the Java codebase- to define it; in your case, a String will do.
The schema:
type Query{
obras: [Obra]
obrasPorArtista(apellidoArtista:String!): [Obra]
}
type Mutation{
addObra(nueva: ObraInput): Obra
}
type Obra{
id: ID
titulo: String
precio: Float
}
input ObraInput{
titulo: String
artista: String
precio: Float
}
The Controller (the service is injected):
#MutationMapping
public Mono<Obra> addObra(#Argument ObraInput nueva){
return obraService.guardarObra(nueva);
}
The ObraInput:
public record ObraInput(String titulo, String artista, double precio) {}
(The Obra is an Entity with the JPA annotations, columns, etc)
Hope it helps!

Kotlin spring boot #RequestBody validation is not triggered

I have a problem on a project with validating #RequestBody by using
implementation("org.springframework.boot:spring-boot-starter-validation")
My DTO looks like this:
import javax.validation.constraints.Email
import javax.validation.constraints.Pattern
class LoginDto(
#Email
val email: String,
#Pattern(regexp = Constants.PASSWORD_REGEX)
val password: String
)
And Controller looks like this:
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import javax.validation.Valid
#RestController
#Validated
class AuthController(private val authService: AuthService) {
#PostMapping("login")
fun login(#Valid #RequestBody loginDto: LoginDto): LoginResponse {
return authService.login(loginDto)
}
...
}
And there is no error from validation, if I try to pass invalid data:
{
"password":"hello",
"email":"dfdfdfdf"
}
I get no error
I use Exposed instead of jpa but I think it's not related to the problem
You should change the annotations of #email and #Pattern to #field:Email and #field:Pattern for example.
The reason for this is twofold, on the one hand you place the annotations on Kotlin properties, and Kotlin properties kan be accessed in a variety of ways. Therefore, you need to specify how you want to access the property to apply the annotation on. On the other hand, the annotations have a set of predefined targets. You can inspect the annotation to see for example that it has a target of field. That's why we can use the #field:Pattern and #field:Email.
This is a key difference with java, where you have have distinct getters, setters, and fields amongst others.

The method findAll() in the type CrudRepository<Build,ObjectId> is not applicable for the arguments (Predicate)

I want to upgrade my spring boot project to 2.6.6 from 1.5.22.RELEASE while upgrading I'm getting the following errors suggest me how to fix it
The method findAll() in the type CrudRepository<Build,ObjectId> is not applicable for the arguments (Predicate)
The method findAll() in the type CrudRepository<Build,ObjectId> is not applicable for the arguments (Predicate, PageRequest)
Repository:
package com.capitalone.dashboard.repository;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
public interface BuildRepository extends CrudRepository<Build, ObjectId>, QueryDslPredicateExecutor<Build>
{
Build findByCollectorItemIdAndNumber(ObjectId collectorItemId, String number);
Build findByCollectorItemIdAndBuildUrl(ObjectId collectorItemId, String buildUrl);
...
}
Client code:
Iterable<Build> result;
if (request.getMax() == null) {
result = buildRepository.findAll(builder.getValue());
} else {
PageRequest pageRequest =PageRequest.of(0, request.getMax(), Sort.Direction.DESC, "timestamp");
result = buildRepository.findAll(builder.getValue(), pageRequest).getContent();
}
build.class
#Document(collection="builds")
public class Build extends BaseModel {
private ObjectId collectorItemId;
private long timestamp;
private String number;
I also tried changing to findAllById then got the below error :
The method findAllById() in the type CrudRepository<Build,ObjectId> is not applicable for the arguments (Predicate)
though the interface is extending QueryDslPredicateExecutor why I'm not able to use findAll(predicate)?
Looking at the API doc for CrudRepository and JpaRepository, I see that findaAll(Example<s> example) is only available in the JpaRepository interface. (I assume builder.getValue() in your code is a single value.)
For CrudRepository you would need to use findAllById(Iterable<ID> ids).
Thus, I suggest switching to JpaRepository or using findAllById.

Javax Validation Custom enum constrains in Kotlin

I'm trying to create a custom annotation and validator to use in conjunction with the javax validation Api and I'm having trouble access the values of an enum.
The objective of the annotation and the validator is validate if an input data is present within the enum values.
This is the annotation class
import javax.validation.Constraint
import javax.validation.Payload
import kotlin.reflect.KClass
#kotlin.annotation.Target(
AnnotationTarget.FIELD,
)
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
#MustBeDocumented
#Constraint(validatedBy = [ValueOfEnumValidator::class])
annotation class ValueOfEnum(
val enumClass: KClass<Enum<*>>,
val message: String ="",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)
This is the validator implementation
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
class ValueOfEnumValidator: ConstraintValidator<ValueOfEnum, CharSequence> {
private val acceptedValues: MutableList<String> = mutableListOf()
override fun initialize(constraintAnnotation: ValueOfEnum) {
super.initialize(constraintAnnotation)
acceptedValues.addAll(constraintAnnotation.enumClass.java
.enumConstants
.map {it.name}
)
}
override fun isValid(value: CharSequence?, context: ConstraintValidatorContext): Boolean {
return if (value == null) {
true
} else acceptedValues.contains(value.toString())
}
}
I'm aiming to use annotation like this:
#field:ValueOfEnum(enumClass = SortDirectionEnum::class, message = "{variants.sorted.sort.direction.not.valid}")
var sortDirection:String?=
But my IDE is reporting me the following error in the enumClass parameter
Type mismatch.
Required:KClass<Enum<*>>
Found:KClass<SortDirectionEnum>
How can I make the annotation generic enough to support different enums, and fix this issue ?
You are restricting enumClass to instances of Enum<*>, allowing Enum instances (Enum is an abstract class though, so nothing can be used) with all types of data, you however want to also allow child classes of Enum, which can be achieved with the out keyword there.
val enumClass: KClass<out Enum<*>>,
https://kotlinlang.org/docs/generics.html

Spring Validation of JSON - Why do I need to add `#field`

I've finally made some progress on Spring validation (on a JSON object coming in from RabbitMQ).
However there are a couple of things I don't understand:
In the documentation, it states I can just use the annotation #NotBlank then in my method I use the annotation #Valid. However I find this wasn't doing anything. So instead I did #field:NotBlank and it worked together with the following - why did this #field do the trick?
#JsonIgnoreProperties(ignoreUnknown = true)
data class MyModel (
#field:NotBlank(message = "ID cannot be blank")
val id : String = "",
#field:NotBlank(message = "s3FilePath cannot be blank")
val s3FilePath : String = ""
)
Then the function using this model:
#Service
class Listener {
#RabbitListener(queues = ["\${newsong.queue}"])
fun received(data: MyModel) {
val factory = Validation.buildDefaultValidatorFactory()
val validator = factory.validator
val validate = validator.validate(data)
// Then this `validate` will return an array of validation errors
println(validate)
}
}
Correct me if I'm wrong however I assumed just using #Valid and this point fun received(#Valid data: MyModel) it would just throw some exception for me to catch - any idea based on my code why this could have been?
Any advice/help would be appreciated.
Thanks.
Here are the imports:
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import javax.validation.*
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.amqp.rabbit.annotation.RabbitListener
import javax.validation.constraints.NotBlank
Quoting Kotlin's documentation for annotations:
When you're annotating a property or a primary constructor parameter, there are multiple Java elements which are generated from the corresponding Kotlin element, and therefore multiple possible locations for the annotation in the generated Java bytecode. To specify how exactly the annotation should be generated, use the following syntax:
class Example(#field:Ann val foo, // annotate Java field
#get:Ann val bar, // annotate Java getter
#param:Ann val quux) // annotate Java constructor parameter
So, until explicitly mention what you are annotating (field, getter or something else) in Kotlin class constructor, it won't automatically know where you want to put that annotation.

Resources