Spring Data REST Custom Resource URI works for String but not Long
Asked Answered
P

5

12

I have a model:

public class MyModel {
    @Id private Long id;
    private Long externalId;
    // Getters, setters
}

I'd like to use externalId as my resource identifier:

@Configuration
static class RepositoryEntityLookupConfig extends RepositoryRestConfigurerAdapter {
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration configuration) {
        configuration
                .withEntityLookup()
                    .forRepository(MyRepository.class, MyModel::getExternalId, MyRepository::findByExternalId);
    }
}

If externalId is a String, this works fine. But since it's a number (Long)

public interface MyRepository extends JpaRepository<MyModel, Long> {
    Optional<MyModel> findByExternalId(@Param("externalId") Long externalId);
}

when invoking: /myModels/1 I get:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Long
    at org.springframework.data.rest.core.config.EntityLookupConfiguration$RepositoriesEntityLookup.lookupEntity(EntityLookupConfiguration.java:213) ~[spring-data-rest-core-2.6.4.RELEASE.jar:na]
    at org.springframework.data.rest.core.support.UnwrappingRepositoryInvokerFactory$UnwrappingRepositoryInvoker.invokeFindOne(UnwrappingRepositoryInvokerFactory.java:130) ~[spring-data-rest-core-2.6.4.RELEASE.jar:na]
    at org.springframework.data.rest.webmvc.RepositoryEntityController.getItemResource(RepositoryEntityController.java:524) ~[spring-data-rest-webmvc-2.6.4.RELEASE.jar:na]
    at org.springframework.data.rest.webmvc.RepositoryEntityController.getItemResource(RepositoryEntityController.java:335) ~[spring-data-rest-webmvc-2.6.4.RELEASE.jar:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_111]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_111]
    ...

A separate custom EntityLookupSupport<MyModel> component class works.

Am I missing something to get it working for Long using method references in my RepositoryRestConfigurerAdapter?

Peer answered 22/7, 2017 at 9:7 Comment(3)
What is the type of the underlying database column? Is the column a String?Costumer
@Ben Database column type: int, Database: MySQL. If the type were String, it works (no converter needed, model field would be String instead of Long). I need it to work with type int (foreign key constraints).Peer
What if you call /myModels/1L instead of /myModels/1 ? It may be a serialization issueInternationalism
M
1

The signature of the method you are trying to call seems to be:

forRepository(Class<R> type, Converter<T,ID> identifierMapping, 
         EntityLookupRegistrar.LookupRegistrar.Lookup<R,ID> lookup)

I don't see how MyModel::getExternalId can be doing the necessary conversion.

I would try something like the following:

@Configuration
static class RepositoryEntityLookupConfig extends RepositoryRestConfigurerAdapter {
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration configuration) {
    configuration
                .withEntityLookup()
                    .forRepository(MyRepository.class, Long::parseLong, MyRepository::findByExternalId);
    }
}
Moretta answered 31/7, 2017 at 17:11 Comment(0)
R
0

Try to add this to your RepositoryEntityLookupConfig class:

@Override
public void configureConversionService(ConfigurableConversionService conversionService) {
    conversionService.addConverter(String.class, Long.class, Long::parseLong);
    super.configureConversionService(conversionService);
}
Ricotta answered 22/7, 2017 at 11:44 Comment(1)
Added it. I still get "java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Long".Peer
I
0

Do you really need to set configuration by yourself ? You could try to use spring-boot auto-configuration by adding @RepositoryRestResource annotation

@RepositoryRestResource(collectionResourceRel = "myModels", path = "myModels")
public interface MyRepository extends JpaRepository<MyModel, Long> {
        Optional<MyModel> findByExternalId(@Param("externalId") Long externalId);
}

Also add @Entity on your model class

@Entity
public class MyModel {
    @Id 
    private Long id;
    @Column(name = "EXTERNAL_ID")
    // Column annotation is not required if you respect case-sensitive
    private Long externalId;
    // Getters, setters
}
Impunity answered 31/7, 2017 at 16:41 Comment(1)
Didn't work for me. Adding the @RepositoryRestResource on MyRepository (while removing the configuration) results in the same effect as the default findById. Model is already annotated with @Entity and column doesn't require annotation (findByExternalId otherwise works).Peer
G
0

Apparently, the default BackendIdConverter (see DefaultIdConverter) does nothing with ID conversion and on the other hand Spring Data Rest cannot use the repository ID type. So, you have to either convert it yourself or configure your custom ID converter bean, for example:

@Bean
public BackendIdConverter myModelBackendIdConverter() {
  return new BackendIdConverter() {

    @Override
    public Serializable fromRequestId(final String id, final Class<?> entityType) {
      return Optional.ofNullable(id).map(Long::parseLong).orElse(null);
    }

    @Override
    public boolean supports(final Class<?> delimiter) {
      return MyModel.class.isAssignableFrom(delimiter);
    }

    @Override
    public String toRequestId(final Serializable id, final Class<?> entityType) {
      return Optional.ofNullable(id).map(Object::toString).orElse(null);
    }
  };
}

See also:

  • BackendIdHandlerMethodArgumentResolver
  • @BackendId
Grebe answered 2/4, 2018 at 1:11 Comment(0)
D
0

try to add @PathVariable("externalId") Long externalId or if you need a solution that "works" you can let it accept string then change it to int :

@GetMapping("/myModels/{externalId}") public ResponseEntity getModelByExternalId(@PathVariable String externalId) { Long id = Long.valueOf(externalId); Optional model = myRepository.findByExternalId(id); return model.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); }

Dissidence answered 11/8 at 16:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.