Unfortunately, all versions of Spring Data JPA / Rest prior to 2.1.0.RELEASE cannot satisfy your needs out of the box. Source buried inside Spring Data Commons / JPA. Spring JPA data only supports Id and EmbeddedId .
Excerpt JpaPersistentPropertyImpl :
static {
Spring Data Commons does not support the concept of combined properties. It considers each property of a class independently of each other.
Of course you can hack Spring Data Rest. But this is cumbersome, does not solve the problem in your heart and reduces the flexibility of the structure.
Here's a hack. This should give you an idea of โโhow to solve your problem.
In your configuration, override repositoryExporterHandlerAdapter and return CustomPersistentEntityResourceAssemblerArgumentResolver . Also, override the backendIdConverterRegistry and add CustomBackendIdConverter to the list of known id converter :
import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.rest.core.projection.ProxyProjectionFactory; import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter; import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; import org.springframework.data.rest.webmvc.spi.BackendIdConverter; import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver; import org.springframework.data.web.config.EnableSpringDataWebSupport; import org.springframework.hateoas.ResourceProcessor; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.plugin.core.OrderAwarePluginRegistry; import org.springframework.plugin.core.PluginRegistry; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @Configuration @Import(RepositoryRestMvcConfiguration.class) @EnableSpringDataWebSupport public class RestConfig extends RepositoryRestMvcConfiguration { @Autowired(required = false) List<ResourceProcessor<?>> resourceProcessors = Collections.emptyList(); @Autowired ListableBeanFactory beanFactory; @Override @Bean public PluginRegistry<BackendIdConverter, Class<?>> backendIdConverterRegistry() { List<BackendIdConverter> converters = new ArrayList<BackendIdConverter>(3); converters.add(new CustomBackendIdConverter()); converters.add(BackendIdConverter.DefaultIdConverter.INSTANCE); return OrderAwarePluginRegistry.create(converters); } @Bean public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() { List<HttpMessageConverter<?>> messageConverters = defaultMessageConverters(); configureHttpMessageConverters(messageConverters); RepositoryRestHandlerAdapter handlerAdapter = new RepositoryRestHandlerAdapter(defaultMethodArgumentResolvers(), resourceProcessors); handlerAdapter.setMessageConverters(messageConverters); return handlerAdapter; } private List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers() { CustomPersistentEntityResourceAssemblerArgumentResolver peraResolver = new CustomPersistentEntityResourceAssemblerArgumentResolver( repositories(), entityLinks(), config().projectionConfiguration(), new ProxyProjectionFactory(beanFactory)); return Arrays.asList(pageableResolver(), sortResolver(), serverHttpRequestMethodArgumentResolver(), repoRequestArgumentResolver(), persistentEntityArgumentResolver(), resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE, peraResolver, backendIdHandlerMethodArgumentResolver()); } }
Create a CustomBackendIdConverter . This class is responsible for providing your custom object identifiers:
import org.springframework.data.rest.webmvc.spi.BackendIdConverter; import java.io.Serializable; public class CustomBackendIdConverter implements BackendIdConverter { @Override public Serializable fromRequestId(String id, Class<?> entityType) { return id; } @Override public String toRequestId(Serializable id, Class<?> entityType) { if(entityType.equals(Customer.class)) { Customer c = (Customer) id; return c.getId() + "_" +c.getStartVersion(); } return id.toString(); } @Override public boolean supports(Class<?> delimiter) { return true; } }
CustomPersistentEntityResourceAssemblerArgumentResolver in turn should return a CustomPersistentEntityResourceAssembler :
import org.springframework.core.MethodParameter; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.projection.ProjectionDefinitions; import org.springframework.data.rest.core.projection.ProjectionFactory; import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler; import org.springframework.data.rest.webmvc.config.PersistentEntityResourceAssemblerArgumentResolver; import org.springframework.data.rest.webmvc.support.PersistentEntityProjector; import org.springframework.hateoas.EntityLinks; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; public class CustomPersistentEntityResourceAssemblerArgumentResolver extends PersistentEntityResourceAssemblerArgumentResolver { private final Repositories repositories; private final EntityLinks entityLinks; private final ProjectionDefinitions projectionDefinitions; private final ProjectionFactory projectionFactory; public CustomPersistentEntityResourceAssemblerArgumentResolver(Repositories repositories, EntityLinks entityLinks, ProjectionDefinitions projectionDefinitions, ProjectionFactory projectionFactory) { super(repositories, entityLinks,projectionDefinitions,projectionFactory); this.repositories = repositories; this.entityLinks = entityLinks; this.projectionDefinitions = projectionDefinitions; this.projectionFactory = projectionFactory; } public boolean supportsParameter(MethodParameter parameter) { return PersistentEntityResourceAssembler.class.isAssignableFrom(parameter.getParameterType()); } public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { String projectionParameter = webRequest.getParameter(projectionDefinitions.getParameterName()); PersistentEntityProjector projector = new PersistentEntityProjector(projectionDefinitions, projectionFactory, projectionParameter); return new CustomPersistentEntityResourceAssembler(repositories, entityLinks, projector); } }
CustomPersistentEntityResourceAssembler must be overridden by getSelfLinkFor . As you can see, entity.getIdProperty() returns the id or startVersion property of your Customer class, which, in turn, is used to retrieve the real value using BeanWrapper . Here we short-circuit the entire structure using the instanceof operator. Therefore, your Customer class must implement Serializable for further processing.
import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.model.BeanWrapper; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler; import org.springframework.data.rest.webmvc.support.Projector; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.Link; import org.springframework.util.Assert; public class CustomPersistentEntityResourceAssembler extends PersistentEntityResourceAssembler { private final Repositories repositories; private final EntityLinks entityLinks; public CustomPersistentEntityResourceAssembler(Repositories repositories, EntityLinks entityLinks, Projector projector) { super(repositories, entityLinks, projector); this.repositories = repositories; this.entityLinks = entityLinks; } public Link getSelfLinkFor(Object instance) { Assert.notNull(instance, "Domain object must not be null!"); Class<? extends Object> instanceType = instance.getClass(); PersistentEntity<?, ?> entity = repositories.getPersistentEntity(instanceType); if (entity == null) { throw new IllegalArgumentException(String.format("Cannot create self link for %s! No persistent entity found!", instanceType)); } Object id;
What is it! You should see these URIs:
{ "_embedded" : { "customers" : [ { "name" : "test", "_links" : { "self" : { "href" : "http://localhost:8080/demo/customers/1_1" } } } ] } }
Imho, if you are working on a project with a green field, I would suggest to push through IdClass completely and go with technical simple identifiers based on Long class. This has been tested with Spring Data Rest 2.1.0.RELEASE, Spring Data JPA 1.6.0.RELEASE and Spring Framework 4.0.3.RELEASE.