【问题标题】:Customizing HATEOAS link generation for entities with composite ids为具有复合 ID 的实体自定义 HATEOAS 链接生成
【发布时间】:2014-07-11 04:20:40
【问题描述】:

我在PageAndSortingRepository 上配置了一个RepositoryRestResource,该PageAndSortingRepository 访问一个包含复合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://&lt;server&gt;/api/customers/1_1" 访问服务器时,我以 json 形式返回正确的资源,但 _links 部分中 self 的 href 是错误的,对于我查询的任何其他客户也是如此:"http://&lt;server&gt;/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,但我很高兴如何更改此默认行为。

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

知道春天的人可以帮帮我吗?

【问题讨论】:

    标签: spring spring-mvc spring-data-jpa spring-hateoas


    【解决方案1】:

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

    摘录JpaPersistentPropertyImpl

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

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

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

    这里是黑客。这应该让您知道如何解决您的问题。

    在您的配置中覆盖repositoryExporterHandlerAdapter 并返回CustomPersistentEntityResourceAssemblerArgumentResolver。 此外,覆盖backendIdConverterRegistry 并将CustomBackendIdConverter 添加到已知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:

    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

    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 以进行进一步处理。

    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:

    {
      "_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 测试的。

    【讨论】:

    • 没问题。这是一个有趣的问题。希望能帮助到你。也许你需要实现fromRequestId 来反序列化你的id。
    • 好答案!我还没有尝试过,但是这个存储库会接受 POST 请求吗?如何通过 REST API 插入数据?
    【解决方案2】:

    首先,创建一个 SpringUtil 来从 spring 中获取 bean。

    import org.springframework.beans.BeansException;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.stereotype.Component;
    
    @Component
    public class SpringUtil implements ApplicationContextAware {
        private static ApplicationContext applicationContext;
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            if(SpringUtil.applicationContext == null) {
                SpringUtil.applicationContext = applicationContext;
            }
        }
    
        public static ApplicationContext getApplicationContext() {
            return applicationContext;
        }
    
        public static Object getBean(String name){
            return getApplicationContext().getBean(name);
        }
    
        public static <T> T getBean(Class<T> clazz){
            return getApplicationContext().getBean(clazz);
        }
    
        public static <T> T getBean(String name,Class<T> clazz){
            return getApplicationContext().getBean(name, clazz);
        }
    }
    

    然后,实现 BackendIdConverter。

    import com.alibaba.fastjson.JSON;
    import com.example.SpringUtil;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
    import org.springframework.stereotype.Component;
    
    import javax.persistence.EmbeddedId;
    import javax.persistence.Id;
    import java.io.Serializable;
    import java.io.UnsupportedEncodingException;
    import java.lang.reflect.Method;
    import java.net.URLDecoder;
    import java.net.URLEncoder;
    
    @Component
    public class CustomBackendIdConverter implements BackendIdConverter {
    
        @Override
        public boolean supports(Class<?> delimiter) {
            return true;
        }
    
        @Override
        public Serializable fromRequestId(String id, Class<?> entityType) {
            if (id == null) {
                return null;
            }
    
            //first decode url string
            if (!id.contains(" ") && id.toUpperCase().contains("%7B")) {
                try {
                    id = URLDecoder.decode(id, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            }
    
            //deserialize json string to ID object
            Object idObject = null;
            for (Method method : entityType.getDeclaredMethods()) {
                if (method.isAnnotationPresent(Id.class) || method.isAnnotationPresent(EmbeddedId.class)) {
                    idObject = JSON.parseObject(id, method.getGenericReturnType());
                    break;
                }
            }
    
            //get dao class from spring
            Object daoClass = null;
            try {
                daoClass = SpringUtil.getBean(Class.forName("com.example.db.dao." + entityType.getSimpleName() + "DAO"));
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
    
            //get the entity with given primary key
            JpaRepository simpleJpaRepository = (JpaRepository) daoClass;
            Object entity = simpleJpaRepository.findOne((Serializable) idObject);
            return (Serializable) entity;
    
        }
    
        @Override
        public String toRequestId(Serializable id, Class<?> entityType) {
            if (id == null) {
                return null;
            }
    
            String jsonString = JSON.toJSONString(id);
    
            String encodedString = "";
            try {
                encodedString = URLEncoder.encode(jsonString, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return encodedString;
        }
    }
    

    之后。你可以为所欲为。

    下面有一个示例。

    • 如果实体具有单个属性 pk,您可以使用 localhost:8080/demo/1 正常。根据我的代码,假设 pk 有注释“@Id”。
    • 如果实体已经组成了pk,假设pk是demoId类型,并且有 注解“@EmbeddedId”,可以使用 localhost:8080/demo/{demoId json} 获取/放置/删除。并且您的自我链接将是相同的。

    【讨论】:

      【解决方案3】:

      虽然不可取,但我已通过在我的 JPA 实体上使用 @EmbeddedId 而不是 IdClass 注释来解决此问题。

      像这样:

      @Entity
      public class Customer {
          @EmbeddedId
          private CustomerId id;
          ...
      }
      
      public class CustomerId {
      
          @Column(...)
          BigInteger key;
          @Column(...)
          int startVersion;
          ...
      }
      

      我现在在返回的实体上看到正确生成的链接1_1

      如果有人仍然可以指导我找到不需要我更改模型表示的解决方案,我们将不胜感激。幸运的是,我的应用程序开发没有取得太大进展,因此在更改时会引起严重关注,但我想对于其他人来说,执行这样的更改会产生很大的开销:(例如,更改在 JPQL 中引用此模型的所有查询查询)。

      【讨论】:

      • 我知道这是一篇旧帖子,但我想我会补充一点,您可以将 @Transient 添加到 getKey 方法中,该方法将在 Customer 中返回 id.key,这将允许您可以快速访问您的 API,但不会影响您的 REST 表示。它很丑,但让你的 API 更干净。
      • 恕我直言最好的方法
      【解决方案4】:

      我遇到了一个类似的问题,即数据休息的复合键场景不起作用。 @ksokol 详细解释为解决问题提供了必要的输入。主要为 data-rest-webmvc 和 data-jpa 更改了我的 pom

          <dependency>
              <groupId>org.springframework.data</groupId>
              <artifactId>spring-data-rest-webmvc</artifactId>
              <version>2.2.1.RELEASE</version>
          </dependency>
      
          <dependency>
              <groupId>org.springframework.data</groupId>
              <artifactId>spring-data-jpa</artifactId>
              <version>1.7.1.RELEASE</version>
          </dependency>
      

      它解决了与复合键相关的所有问题,我不需要进行自定义。感谢 ksokol 的详细解释。

      【讨论】:

      • 您好@Alagesan,您知道解决问题的Spring Data JPA 还是REST 版本吗?谢谢
      猜你喜欢
      • 1970-01-01
      • 2013-02-08
      • 2015-09-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-26
      • 2021-12-29
      • 2015-04-18
      相关资源
      最近更新 更多