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

u0njafvf  于 2022-11-10  发布在  Spring
关注(0)|答案(5)|浏览(150)

我已经在PageAndSortingRepository上配置了一个RepositoryRestResource,它访问一个包含复合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,但self_links部分中的href是错误的,而且对于我查询的任何其他客户也是一样的:"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,但我很高兴我可以改变这种默认行为。
我已经看过ResourceSupportResourceProcessor类,但不确定我需要修改多少才能解决这个问题。
能有个懂Spring的人帮我一把吗?

0s7z1bwu

0s7z1bwu1#

不幸的是,所有的Spring Data JPA/Rest版本2.1.0.RELEASE都不能满足您的需求。源代码隐藏在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;
}

SpringDataCommons不支持组合属性的概念,它将一个类的每个属性彼此独立地对待。
当然,您可以破解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"
        }
      }
    } ]
  }
}

Imho,如果你正在做一个绿色领域的项目,我建议你完全抛弃IdClass,使用基于Long类的技术简单的id。这已经用Spring Data Rest 2.1.0.RELEASE,Spring Data JPA 1.6.0.RELEASE和Spring Framework 4.0.3.RELEASE测试过了。

mpgws1up

mpgws1up2#

虽然不太理想,但我已经通过在JPA实体上使用@EmbeddedId而不是IdClass注解来解决这个问题。
就像这样:

@Entity
public class Customer {
    @EmbeddedId
    private CustomerId id;
    ...
}

public class CustomerId {

    @Column(...)
    BigInteger key;
    @Column(...)
    int startVersion;
    ...
}

现在,我在返回的实体上看到了正确生成的链接1_1
如果还有人能指导我找到一个不需要我改变模型表示的解决方案,我将不胜感激。幸运的是,我在应用程序开发方面还没有取得很大进展,因此在改变时需要认真考虑这一点,但我想对于其他人来说,执行这样的改变会有很大的开销:(例如,在JPQL查询中更改引用此模型的所有查询)。

6ojccjat

6ojccjat3#

我也遇到过类似的问题,data rest的复合键场景不起作用。@ksokol详细的解释提供了解决这个问题所需的输入。

<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的详细解释。

unguejic

unguejic4#

首先,创建一个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}来get/put/delete,那么你的self链接也是一样的。
9bfwbjaz

9bfwbjaz5#

上面提供的答案很有帮助,但如果您需要更通用的方法,请遵循以下步骤-

package com.pratham.persistence.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.istack.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;

import javax.persistence.EmbeddedId;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Customization of how composite ids are exposed in URIs.
 * The implementation will convert the Ids marked with {@link EmbeddedId} to base64 encoded json
 * in order to expose them properly within URI.
 *
 * @author im-pratham
 */
@Component
@RequiredArgsConstructor
public class EmbeddedBackendIdConverter implements BackendIdConverter {
    private final ObjectMapper objectMapper;

    @Override
    public Serializable fromRequestId(String id, Class<?> entityType) {
        return getFieldWithEmbeddedAnnotation(entityType)
                .map(Field::getType)
                .map(ret -> {
                    try {
                        String decodedId = new String(Base64.getUrlDecoder().decode(id));
                        return (Serializable) objectMapper.readValue(decodedId, (Class) ret);
                    } catch (JsonProcessingException ignored) {
                        return null;
                    }
                })
                .orElse(id);
    }

    @Override
    public String toRequestId(Serializable id, Class<?> entityType) {
        try {
            String json = objectMapper.writeValueAsString(id);
            return Base64.getUrlEncoder().encodeToString(json.getBytes(UTF_8));
        } catch (JsonProcessingException ignored) {
            return id.toString();
        }
    }

    @Override
    public boolean supports(@NonNull Class<?> entity) {
        return isEmbeddedIdAnnotationPresent(entity);
    }

    private boolean isEmbeddedIdAnnotationPresent(Class<?> entity) {
        return getFieldWithEmbeddedAnnotation(entity)
                .isPresent();
    }

    @NotNull
    private static Optional<Field> getFieldWithEmbeddedAnnotation(Class<?> entity) {
        return Arrays.stream(entity.getDeclaredFields())
                .filter(method -> method.isAnnotationPresent(EmbeddedId.class))
                .findFirst();
    }
}

相关问题