为具有复合 ID 的实体自定义 HATEOAS 链接生成 [英] Customizing HATEOAS link generation for entities with composite ids

查看:37
本文介绍了为具有复合 ID 的实体自定义 HATEOAS 链接生成的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在 PageAndSortingRepository 上配置了一个 RepositoryRestResource 来访问一个包含复合 Id 的实体:

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

@Entity
@IdClass(CustomerId.class)
public class Customer {
    @Id BigInteger id;
    @Id int startVersion;
    ...
}

public class CustomerId {
    BigInteger id;
    int startVersion;
    ...
}

@RepositoryRestResource(collectionResourceRel = "customers", path = "customers", itemResourceRel = "customers/{id}_{startVersion}")
public interface CustomerRepository extends PagingAndSortingRepository<Customer, CustomerId> {}

例如,当我通过 "http://<server>/api/customers/1_1" 访问服务器时,我将正确的资源作为 json 返回,但 _links 中的 hrefself 部分是错误的,对于我查询的任何其他客户也是如此:"http://<server>/api/customer/1"

When i access the server at "http://<server>/api/customers/1_1" for instance, I get the correct resource back as json, but the href in the _links section for self is the wrong and also the same for any other customer i query: "http://<server>/api/customer/1"

即:

{
  "id" : 1,
  "startVersion" : 1,
  ...
  "firstname" : "BOB",
  "_links" : {
    "self" : {
      "href" : "http://localhost:9081/reps/api/reps/1" <-- This should be /1_1
    }
  }
}

我想这是因为我的复合 ID,但我很高兴如何更改此默认行为.

I suppose this is because of my composite Id, But I am chuffed as to how i can change this default behaviour.

我已经查看了 ResourceSupportResourceProcessor 类,但不确定我需要更改多少才能解决此问题.

I've had a look at the ResourceSupport and the ResourceProcessor class but am not sure how much i need to change in order fix this issue.

懂春天的人可以帮帮我吗?

Can someone who knows spring lend me a hand?

推荐答案

不幸的是,直到 2.1.0.RELEASE 的所有 Spring Data JPA/Rest 版本都无法满足您开箱即用的需求.源代码隐藏在 Spring Data Commons/JPA 本身中.Spring Data JPA 仅支持 IdEmbeddedId 作为标识符.

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.

摘录JpaPersistentPropertyImpl:

static {

    // [...]

    annotations = new HashSet<Class<? extends Annotation>>();
    annotations.add(Id.class);
    annotations.add(EmbeddedId.class);

    ID_ANNOTATIONS = annotations;
}

Spring Data Commons 不支持组合属性的概念.它独立地对待一个类的每个属性.

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

当然,你可以破解 Spring Data Rest.但是这样很麻烦,并没有从根本上解决问题,降低了框架的灵活性.

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.

在您的配置中覆盖 repositoryExporterHandlerAdapter 并返回一个 CustomPersistentEntityResourceAssemblerArgumentResolver.此外,覆盖 backendIdConverterRegistry 并将 CustomBackendIdConverter 添加到已知的 id 转换器 列表:

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<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());
    }
}

创建CustomBackendIdConverter.此类负责呈现您的自定义实体 ID:

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 反过来应该返回一个 CustomPersistentEntityResourceAssembler:

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 需要覆盖 getSelfLinkFor.正如您所看到的,entity.getIdProperty() 返回 Customer 类的 id 或 startVersion 属性,而后者又用于在 BeanWrapper.这里我们使用 instanceof 操作符来短路整个框架.因此,您的 Customer 类应该实现 Serializable 以进行进一步处理.

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<? 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;

        //this is a hack for demonstration purpose. don't do this at home!
        if(instance instanceof Customer) {
            id = instance;
        } else {
            BeanWrapper<Object> 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);
    }
}

就是这样!您应该会看到此 URI:

That's it! You should see this URIs:

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

恕我直言,如果您正在从事绿地项目,我建议您完全放弃 IdClass 并使用基于 Long 类的技术简单 ID.已使用 Spring Data Rest 2.1.0.RELEASE、Spring Data JPA 1.6.0.RELEASE 和 Spring Framework 4.0.3.RELEASE 进行测试.

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.

这篇关于为具有复合 ID 的实体自定义 HATEOAS 链接生成的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆