Customizing HATEOAS link generation for entities with composite ids

前端 未结 4 1728
北荒
北荒 2020-12-05 12:08

I have configured a RepositoryRestResource on a PageAndSortingRepository that accesses an Entity that includes a composite Id:

@Ent         


        
4条回答
  •  温柔的废话
    2020-12-05 12:48

    Unfortunately, all Spring Data JPA/Rest versions up to 2.1.0.RELEASE are not able to serve your need out of the box. The source is buried inside Spring Data Commons/JPA itself. Spring Data JPA supports only Id and EmbeddedId as identifier.

    Excerpt JpaPersistentPropertyImpl:

    static {
    
        // [...]
    
        annotations = new HashSet>();
        annotations.add(Id.class);
        annotations.add(EmbeddedId.class);
    
        ID_ANNOTATIONS = annotations;
    }
    

    Spring Data Commons doesn't support the notion of combined properties. It treats every property of a class independently from each other.

    Of course, you can hack Spring Data Rest. But this is cumbersome, doesn't solve the problem at its heart and reduces the flexibility of the framework.

    Here's the hack. This should give you an idea how to tackle your problem.

    In your configuration override repositoryExporterHandlerAdapter and return a CustomPersistentEntityResourceAssemblerArgumentResolver. Additionally, override 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> resourceProcessors = Collections.emptyList();
        @Autowired
        ListableBeanFactory beanFactory;
    
        @Override
        @Bean
        public PluginRegistry> backendIdConverterRegistry() {
    
            List converters = new ArrayList(3);
            converters.add(new CustomBackendIdConverter());
            converters.add(BackendIdConverter.DefaultIdConverter.INSTANCE);
    
            return OrderAwarePluginRegistry.create(converters);
        }
    
        @Bean
        public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() {
    
            List> messageConverters = defaultMessageConverters();
            configureHttpMessageConverters(messageConverters);
    
            RepositoryRestHandlerAdapter handlerAdapter = new RepositoryRestHandlerAdapter(defaultMethodArgumentResolvers(),
                    resourceProcessors);
            handlerAdapter.setMessageConverters(messageConverters);
    
            return handlerAdapter;
        }
    
        private List 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 CustomBackendIdConverter. This class is responsible for rendering your custom entity ids:

    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 needs to override getSelfLinkFor. As you can see entity.getIdProperty() return either id or startVersion property of your Customer class which in turn gets used to retrieve the real value with the help of a BeanWrapper. Here we are short circuit the whole framework with the use of instanceof operator. Hence your Customer class should 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 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;
    
            //this is a hack for demonstration purpose. don't do this at home!
            if(instance instanceof Customer) {
                id = instance;
            } else {
                BeanWrapper wrapper = BeanWrapper.create(instance, null);
                id = wrapper.getProperty(entity.getIdProperty());
            }
    
            Link resourceLink = entityLinks.linkToSingleResource(entity.getType(), id);
            return new Link(resourceLink.getHref(), Link.REL_SELF);
        }
    }
    
    
    

    That's it! You should see this URIs:

    {
      "_embedded" : {
        "customers" : [ {
          "name" : "test",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/demo/customers/1_1"
            }
          }
        } ]
      }
    }
    

    Imho, if you are working on a green field project I would suggest to ditch IdClass entirely and go with technical simple ids based on Long class. This was tested with Spring Data Rest 2.1.0.RELEASE, Spring data JPA 1.6.0.RELEASE and Spring Framework 4.0.3.RELEASE.

    提交回复
    热议问题