11

I'm trying to implement Interface-based Projection but I cannot make it work with my custom type column.

Below example of what I'm trying to do:

Repository:

@Query(value = "SELECT customType from TABLE", nativeQuery = true)
List<TestClass> getResults();

Interface projection:

public interface TestClass {
  @Convert(converter = MyCustomTypeConverter.class)
  MyCustomType getCustomType();
}

Converter:

@Converter
public class MyCustomTypeConverter implements Converter<String, MyCustomType> {

      @Override
      public MyCustomType convert(String source) {
        // whatever
      }
}

When I call getResults() on repository I receive list of results as expected, but when I try to call getCustomType() on one of results I get exception:

java.lang.IllegalArgumentException: Projection type must be an interface!
at org.springframework.util.Assert.isTrue(Assert.java:118)
at org.springframework.data.projection.ProxyProjectionFactory.createProjection(ProxyProjectionFactory.java:100)
at org.springframework.data.projection.SpelAwareProxyProjectionFactory.createProjection(SpelAwareProxyProjectionFactory.java:45)
at org.springframework.data.projection.ProjectingMethodInterceptor.getProjection(ProjectingMethodInterceptor.java:131)
at org.springframework.data.projection.ProjectingMethodInterceptor.invoke(ProjectingMethodInterceptor.java:80)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.projection.ProxyProjectionFactory$TargetAwareMethodInterceptor.invoke(ProxyProjectionFactory.java:245)

I found that problem lies in

org.springframework.data.projection.ProxyProjectionFactory

which uses

org.springframework.core.convert.support.DefaultConversionService

which obviously doesn't have my custom type converter registered.

If I stop on breakpoint in ConversionService and manually add my converter in runtime, projection will work without any problem.

So question is: can I somehow register my custom converter to ConversionService used by spring jpa during interface-based projection?

EDIT:

I added my converter to DefaultConversionService's sharedInstance in InitializingBean like below and it worked.

@Component
public class DefaultConversionServiceInitializer implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
        DefaultConversionService conversionService = (DefaultConversionService) DefaultConversionService.getSharedInstance();
        conversionService.addConverter(new MyCustomTypeConverter());
    }
}
jprzystupa
  • 113
  • 1
  • 7
  • 1
    I have the same issue, but this solution doesn't work. The custom converter is added to the shared conversionService at the context creation, but still not found while resolving the converters in ProxyProjectionFactory. What version of spring-data are you using ? – Guillaume Apr 15 '20 at 10:29
  • spring boot 2.2.1.RELEASE. did you check if DefaultConversionService contains your converter at the time of conversion? I noticed that MyCustomTypeConverter is not an AttributeConverter but a org.springframework.core.convert.converter.Converter. Maybe that is the problem. I will update my question. – jprzystupa Apr 16 '20 at 12:25
  • Another thing to check is if source type for converter is as you expect. For example, in one case I had to create converter with source type Character instead of String. I would debug it and check what types exactly it tries to convert – jprzystupa Apr 16 '20 at 12:32
  • 1
    I have the issue in a 2.0.4 spring boot, so I guess this is a fixed issue. I have try to debug this opaque system and the shared instance of the conversion service was not used at all in the dynamic proxies build for my repositories. Thanks a lot for your feedback ! the commit that did the change: https://github.com/spring-projects/spring-data-commons/commit/ea8cf8c629bd0c50991801811e1334589c1a4aff#diff-c80238b8ee9becc1b1dc7abaf85a2c4f – Guillaume Apr 16 '20 at 13:09
  • the fix for the issue has been introduced in spring-boot 2.0.9 – Guillaume Apr 17 '20 at 13:31
  • Hi, I get error cannot type cast org.springframework.core.convert.converter.Converter to AttributeConverter. Im using spring 2.0.2. Any idea how to fix it? – RukaDo Jul 01 '20 at 22:10
  • I guess that it's version problem as I'm using 2.2.x. Maybe just try to use AttributeConverter instead of Converter? – jprzystupa Jul 06 '20 at 08:04
  • This is broken again in spring boot, a suggested workaround is to specify a converter in an `@Value` annotation; see https://github.com/spring-projects/spring-data-commons/issues/2260#issuecomment-774029777 – MikaelF Dec 07 '21 at 00:08

2 Answers2

4

The ConversionService used is DefaultConversionService.getSharedInstance().

So you should be able to access that and add your converter.

Jens Schauder
  • 77,657
  • 34
  • 181
  • 348
  • 1
    Ok, it worked. I just needed to cast it to DefaultConversionService beacuse ConversionService has no addConverter() method. – jprzystupa Oct 21 '19 at 09:58
3

This broke again in Spring Data Jpa 2.4.0 (see this closed GitHub issue).

One workaround, as suggested by user ajobra76 (link), is to specify the converter to use like so:

public interface TestClass {
    @Value("#{@myCustomTypeConverter.convert(target.customType)}")
    MyCustomType getCustomType();
}

Where myCustomTypeConverter would be a bean in the ApplicationContext. Of course there are other ways of specifying a method using SpEL, including creating a new object, and calling a static method. But this is just to show that it can be done.

target is an identifier that will be bound to the "aggregate root backing the projection" (Spring Data Jpa docs, section "An Open Projection").

MikaelF
  • 3,518
  • 4
  • 20
  • 33