복붙노트

[SPRING] 복합 ID가있는 엔티티에 대한 HATEOAS 링크 생성 사용자 정의

SPRING

복합 ID가있는 엔티티에 대한 HATEOAS 링크 생성 사용자 정의

복합 ID가 포함 된 엔티티에 액세스하는 PageAndSortingRepository에서 RepositoryRestResource를 구성했습니다.

@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 : // / api / customers / 1_1"에있는 서버에 액세스 할 때 json으로 올바른 자원을 얻었지만 self에 대한 _links 섹션의 href가 잘못되어 동일한 문제가 발생했습니다. 다른 고객 i 쿼리 : "http : // / 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 때문이라고 생각하지만,이 기본 동작을 어떻게 바꿀 수 있는지에 관해서는 기뻐합니다.

ResourceSupport 클래스와 ResourceProcessor 클래스를 살펴 보았지만이 문제를 해결하기 위해 얼마만큼 변경해야하는지 잘 모르겠습니다.

봄을 아는 사람이 저에게 손을 빌려줄 수 있습니까?

해결법

  1. ==============================

    1.안타깝게도 2.1.0.RELEASE까지의 모든 Spring Data JPA / Rest 버전은 사용자의 요구를 즉시 충족시킬 수 없습니다. 소스는 Spring Data Commons / JPA 자체에 파묻혀있다. 스프링 데이터 JPA는 식별자로 ID와 EmbeddedId 만 지원합니다.

    안타깝게도 2.1.0.RELEASE까지의 모든 Spring Data JPA / Rest 버전은 사용자의 요구를 즉시 충족시킬 수 없습니다. 소스는 Spring Data Commons / JPA 자체에 파묻혀있다. 스프링 데이터 JPA는 식별자로 ID와 EmbeddedId 만 지원합니다.

    발췌문 JpaPersistentPropertyImpl :

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

    Spring Data Commons는 결합 된 속성의 개념을 지원하지 않습니다. 그것은 클래스의 모든 속성을 서로 독립적으로 취급합니다.

    물론 Spring 데이터 나머지를 해킹 할 수 있습니다. 그러나 이것은 성 가시고 문제를 해결하지 못하고 프레임 워크의 유연성을 감소시킵니다.

    여기 해킹이 있습니다. 이렇게하면 문제를 해결할 수있는 방법을 알 수 있습니다.

    구성에서 repositoryExporterHandlerAdapter를 대체하고 CustomPersistentEntityResourceAssemblerArgumentResolver를 리턴하십시오. 또한 backendIdConverterRegistry를 재정의하고 CustomBackendIdConverter를 알려진 id 변환기 목록에 추가합니다.

    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 연산자를 사용하여 전체 프레임 워크를 단락합니다. 따라서 고객 클래스는 추가 처리를 위해 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 데이터 JPA 1.6.0.RELEASE 및 Spring Framework 4.0.3.RELEASE로 테스트되었습니다.

  2. ==============================

    2.바람직한 것은 아니지만, JPA 엔티티에서 IdClass 주석 대신 @EmbeddedId를 사용하여이 문제를 해결했습니다.

    바람직한 것은 아니지만, JPA 엔티티에서 IdClass 주석 대신 @EmbeddedId를 사용하여이 문제를 해결했습니다.

    이렇게 :

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

    반환 된 항목에서 올바르게 생성 된 링크 1_1을 볼 수 있습니다.

    누구든지 내 모델의 표현을 변경하지 않아도되는 솔루션으로 안내 할 수 있다면 매우 높이 평가할 것입니다. 다행스럽게도 나는 이것이 변경에 심각한 우려가 될 수있는 내 응용 프로그램 개발에서 발전하지는 않았지만, 다른 사람들에게는 다음과 같이 변경을 수행하는 데 상당한 오버 헤드가 발생할 것이라고 상상합니다. (예 : JPQL에서이 모델을 참조하는 모든 쿼리 변경 쿼리).

  3. ==============================

    3.데이터 휴지의 복합 키 시나리오가 작동하지 않는 비슷한 문제가있었습니다. @ksokol 상세한 설명은 문제를 해결하는 데 필요한 정보를 제공했습니다. 주로 데이터 rest-webmvc 및 data-jpa에 대한 내 pom 변경

    데이터 휴지의 복합 키 시나리오가 작동하지 않는 비슷한 문제가있었습니다. @ksokol 상세한 설명은 문제를 해결하는 데 필요한 정보를 제공했습니다. 주로 데이터 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에게 감사드립니다.

  4. ==============================

    4.먼저 SpringUtil을 만들어 Spring에서 Bean을 가져온다.

    먼저 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;
        }
    }
    

    그 후. 당신이 원하는 것을 할 수 있습니다.

    아래 샘플이 있습니다.

  5. from https://stackoverflow.com/questions/23801575/customizing-hateoas-link-generation-for-entities-with-composite-ids by cc-by-sa and MIT license