Skip to content

Spring SPEL expressions in @Query seems to have broken #2125

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
sureshpw opened this issue Jan 22, 2021 · 5 comments
Closed

Spring SPEL expressions in @Query seems to have broken #2125

sureshpw opened this issue Jan 22, 2021 · 5 comments
Labels
status: on-hold We cannot start working on this issue yet

Comments

@sureshpw
Copy link

This was working with earlier version of Spring Boot (2.3.4). Giving conversion error in latest version (2.4.2 using SDN 6.0.3). Any suggestions?

@Node("person")
@lombok.Data
public static class Person {
    @GeneratedValue @Id
    private Long id;
    String name;
}   
public interface PersonRepository extends Neo4jRepository<Person, Long> {
    @Query("MATCH (p: Person { p.name: :#{#person.name} }) RETURN p")
    Optional<Person> get(Person person);
}
class Neo4jApplicationTests {
    @Autowired
    private PersonRepository repo;

    @Test
    public void testGet() {
        Person person = new Person(); 
        person.setName("person-1"); 
    
        Optional<Person> pp = repo.get(person);
    }
}

org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [Person] to type [org.neo4j.driver.Value]
	at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:322)
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:195)
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:175)
	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jConversionService.lambda$writeValue$3(DefaultNeo4jConversionService.java:106)
	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jConversionService.writeValueImpl(DefaultNeo4jConversionService.java:124)
	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jConversionService.writeValue(DefaultNeo4jConversionService.java:107)
	at org.springframework.data.neo4j.repository.query.Neo4jQuerySupport.convertParameter(Neo4jQuerySupport.java:163)
	at org.springframework.data.neo4j.repository.query.Neo4jQuerySupport.convertParameter(Neo4jQuerySupport.java:125)
	at org.springframework.data.neo4j.repository.query.StringBasedNeo4jQuery.lambda$bindParameters$5(StringBasedNeo4jQuery.java:208)
	at java.base/java.lang.Iterable.forEach(Iterable.java:75)
	at org.springframework.data.neo4j.repository.query.StringBasedNeo4jQuery.bindParameters(StringBasedNeo4jQuery.java:205)
	at org.springframework.data.neo4j.repository.query.StringBasedNeo4jQuery.prepareQuery(StringBasedNeo4jQuery.java:170)
	at org.springframework.data.neo4j.repository.query.AbstractNeo4jQuery.execute(AbstractNeo4jQuery.java:82)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:152)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:131)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jan 22, 2021
@michael-simons
Copy link
Collaborator

Hi. It has worked by coincidence, as it serializes the whole Person object to JSON via Jackson as long as that is possible (Jackson being present and the thing being serializable).

Why the whole person? The SpEL expression cannot know if the parameter has been used in another part of the query as well. So we always pass it in as a whole.

Here a couple of ways to make this work in SDN 6:

import java.util.Optional;

import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;

public interface PersonRepository extends Neo4jRepository<Person, Long> {

	// 1. Use a derived finder method
	Optional<Person> findOneByName(String name);

	// 2. find by example comes with the repo

	// 3. use spel but without passing the whole object
	@Query("MATCH (p: Person { name: :#{#name} }) RETURN p")
	Optional<Person> get(String name);

	// 4. register a custom converter
	@Query("MATCH (p: Person { name: :#{#person.name} }) RETURN p")
	Optional<Person> get(Person person);
}

I work you through:

Use a derived finder method for that simple purpose:

Probably the easiest one. Method needs to be declared

Use findByExample

That is automatically on the repo, probably the most similar to what you have

Simple Spel

Just pass the name. Map of known java types works as well.

Register a converter for the person

package com.example.issue0122;

import java.util.Arrays;
import java.util.Map;

import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.neo4j.core.convert.Neo4jConversions;

@Configuration
public class AdditionalNeo4jConverters {

	@Bean
	public Neo4jConversions neo4jConversions() {
		return new Neo4jConversions(Arrays.asList(new PersonToValueConverter()));
	}

	static class PersonToValueConverter implements Converter<Person, Value> {

		@Override public Value convert(Person source) {

			var convertedPerson = Map.of(
				"id", source.getId() == null ? -1 : source.getId(),
				"name", source.getName()
				// plus whatever else, it does not actually be filled, can be empty to
				// our spel extracts the value of person.name anyway.
			);
			return Values.value(convertedPerson);
		}
	}
}

See the parameters passed to the query

:params {0: {name: "Michael", id: -1, additional: "things"}, __SpEL__0: "Michael", person: {name: "Michael", id: -1, additional: "things"}}

it would be same with SDN 5, but without you being in control for that.

I hope that helps.

@michael-simons michael-simons added blocked: awaiting feedback status: on-hold We cannot start working on this issue yet and removed status: waiting-for-triage An issue we've not yet triaged labels Jan 22, 2021
@sureshpw
Copy link
Author

The argument person to the repository method is just an example for the test case. It could be a filter criteria or some value object etc. In general, one should be able to provide SPEL expressions in query annotations. It used to work before, we have this implemented for over a year and now trying to upgrade to newer versions.

JPA based @Query still works without any problem.
For example: (https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions).

I was under the impression that the @Query and SPEL expressions are common for all spring data projects.

Thanks for the examples.

@michael-simons
Copy link
Collaborator

michael-simons commented Jan 22, 2021 via email

@sureshpw
Copy link
Author

Registering a custom converter is what I have to do to reduce code changes. Thanks for the explanation.

BTW, here is the exact feature request in another spring data project:

spring-projects/spring-data-r2dbc#164

@michael-simons
Copy link
Collaborator

I wonder where my head was last night.

We will convert your entities to complete nested maps in 6.1.

e783dcf

Regardless of that: Spel works exactly as it did with SDN5 and other modules. The above part is a plus.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: on-hold We cannot start working on this issue yet
Projects
None yet
Development

No branches or pull requests

3 participants