为具有复合 ID 的实体自定义 HATEOAS 链接生成

     2023-02-26     65

关键词:

【中文标题】为具有复合 ID 的实体自定义 HATEOAS 链接生成【英文标题】:Customizing HATEOAS link generation for entities with composite ids 【发布时间】: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 类,但不确定我需要进行多少更改才能解决此问题。

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

【问题讨论】:

【参考方案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 版本吗?谢谢

使用自定义 ID 插入数据的 Code-First 实体框架

...问题描述】:我在我的项目中使用代码优先EF,并在插入具有自定义ID的数据时遇到问题。当我尝试插入具有自定义ID(例如999)的数据时,EF会忽略它并将递增的ID插入到表中。我的模特:publicclassAddress[ 查看详情

具有关系的实体框架自定义过程(代码片段)

我有一个名为MenuContainer的类publicpartialclassMenuContainer:IEquatable<MenuContainer>[Column("Id")][Key]publicintIdget;set;publicintMenuIdget;set;publicintWareIdget;set;publicintPriceget;set;publicvirtualMenuMenuget;set;publicvirtualWareWareget;set;publicboolEquals(MenuContainerother)... 查看详情

如何编写自定义函数将文档拆分为具有相同 ID 的多个文档

】如何编写自定义函数将文档拆分为具有相同ID的多个文档【英文标题】:HowtowriteacustomfunctiontosplitadocumentintomultipledocumentsofsameId【发布时间】:2018-03-0914:23:45【问题描述】:我正在尝试拆分具有以下字符串类型字段的文档:"_id":... 查看详情

Spring Boot:HATEOAS 和自定义 JacksonObjectMapper

】SpringBoot:HATEOAS和自定义JacksonObjectMapper【英文标题】:SpringBoot:HATEOASandcustomJacksonObjectMapper【发布时间】:2020-06-2119:16:07【问题描述】:将HATEOAS的依赖项添加到Maven后,SpringBoot无法启动:添加依赖:<dependency><groupId>org.... 查看详情

如何在flutter中将具有文档ID的自定义对象设置为firestore中的集合?

】如何在flutter中将具有文档ID的自定义对象设置为firestore中的集合?【英文标题】:howtosetcustomobjectwithdocumentidtoacollectioninfirestoreinflutter?【发布时间】:2020-10-1703:43:26【问题描述】:我用一些字符串变量和另一个B类的对象列表定... 查看详情

自定义 MongoDB 对象 _id 与复合索引

】自定义MongoDB对象_id与复合索引【英文标题】:CustomMongoDBObject_idvsCompoundindex【发布时间】:2016-12-1507:42:39【问题描述】:所以我需要在MongoDB中创建一个查找集合来验证唯一性。要求是检查是否重复了相同的2个值。在SQL中,我... 查看详情

如何在 Swift 3 中将自定义类保存为 CoreData 实体的属性?

...2009:45:48【问题描述】:我有一个CoreData实体SavedWorkout。它具有以下属性:completionCounter是Bool的数组,workout是一个名为Workout的自定义类 查看详情

如何将具有自定义 ID 的文档添加到 Firestore

】如何将具有自定义ID的文档添加到Firestore【英文标题】:HowtoaddDocumentwithCustomIDtofirestore【发布时间】:2018-07-1012:14:45【问题描述】:是否有机会使用自定义生成的id将文档添加到firestore集合,而不是由firestore引擎生成的id?【问... 查看详情

magento 导出具有自定义字段的产品

】magento导出具有自定义字段的产品【英文标题】:magentoexportproductswithcustomfield【发布时间】:2014-08-2016:48:40【问题描述】:我正在使用数据流-配置文件来导出所有产品。但我需要来自默认列映射器的更多列。我需要以下列实体I... 查看详情

实体框架 linq 主细节投影到自定义类

】实体框架linq主细节投影到自定义类【英文标题】:EntityFrameworklinqmasterdetailprojectionintocustomclasses【发布时间】:2014-12-3112:59:03【问题描述】:我有一个Order类,其属性为“ID为Int64,描述为字符串,详细信息为List(OfOrderDetail)”还... 查看详情

实体框架代码第一个自定义Id

】实体框架代码第一个自定义Id【英文标题】:EntityframeworkcodefirstcustomId【发布时间】:2011-08-0218:47:24【问题描述】:创建模型时我需要有一个自定义ID。例如。这些是我的限制:8位数字。根据限制,ID必须以特定数字开头。如何... 查看详情

如何在firestore 9中将具有自定义ID的文档添加到firestore

】如何在firestore9中将具有自定义ID的文档添加到firestore【英文标题】:HowtoaddDocumentwithCustomIDtofirestoreinfirestore9【发布时间】:2022-01-2100:38:52【问题描述】:如何将自定义id添加到firestore文档而不是firebase9自动生成的id?我无法将... 查看详情

用于将自定义查询映射到具有用户定义的键/值对的分层实体的“规范”方法

】用于将自定义查询映射到具有用户定义的键/值对的分层实体的“规范”方法【英文标题】:"Canonical"approachformappingcustomqueriestohierarchicalentitieswithuser-definedkey/valuepairs【发布时间】:2016-03-2619:10:20【问题描述】:到目前为... 查看详情

Hibernate 对具有复合键的子实体执行错误的插入顺序

】Hibernate对具有复合键的子实体执行错误的插入顺序【英文标题】:Hibernatedoeswronginsertionorderforchildentitieswithcompositekeys【发布时间】:2015-05-1204:35:19【问题描述】:我有一个依赖于插入顺序的表(我无法更改的不良遗留设计)并... 查看详情

cad由一个自定义实体事件中的id得到自定义实体对象(com接口vb语言)

由一个自定义实体事件中的id得到自定义实体对象。该函数只能在自定义实体事件中调用。1234567891011121314151617181920212223242526272829303132333435363738394041424344IfsEventName="MxDrawXCustomEntity::explode"Then     &nb 查看详情

Spring HATEOAS 链接安全

】SpringHATEOAS链接安全【英文标题】:SpringHATEOASLinkSecurity【发布时间】:2016-04-1420:11:01【问题描述】:我有一个关于通过SpringDate/HATEOAS暴露的RESTAPI的安全性的问题:对实体的请求将导致如下响应:"id":1,"someAttr":"val","_links":"someColl... 查看详情

Symfony2:奏鸣曲实体中的自定义标识符

...2509:35:14【问题描述】:我有一个在__construct函数上生成的具有自定义ID(即UUID)的实体。namespaceAppBundle\\Entity;useRhumsaa\\Uuid\\Uuid;useDoctrine\\ORM\\Mappingas 查看详情

级联保存具有外键的实体对象作为复合主键的一部分

】级联保存具有外键的实体对象作为复合主键的一部分【英文标题】:CascadingsaveEntityobjectswithForeignKeyasapartofcompositePrimaryKey【发布时间】:2013-02-2117:34:04【问题描述】:我想将我的QuestionCompletion类的对象与所有子元素一起保留。... 查看详情