복붙노트

[SPRING] 직렬화에서 JPA Lazy Fetching을 트리거하는 Jackson

SPRING

직렬화에서 JPA Lazy Fetching을 트리거하는 Jackson

JPA를 통해 데이터베이스 (PostgreSQL) 데이터를 RESTful API에 노출하는 백엔드 구성 요소가 있습니다.

문제는 JPA 엔티티를 REST 응답으로 보낼 때 Jackson이 모든 Lazy JPA 관계를 트리거하는 것을 볼 수 있다는 것입니다.

코드 예 (간체) :

import org.springframework.hateoas.ResourceSupport;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Entity
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")//for resolving this bidirectional relationship, otherwise StackOverFlow due to infinite recursion
public class Parent extends ResourceSupport implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    private Long id;

    //we actually use Set and override hashcode&equals
    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
    private List<Child> children = new ArrayList<>();

    @Transactional
    public void addChild(Child child) {

        child.setParent(this);
        children.add(child);
    }

    @Transactional
    public void removeChild(Child child) {

        child.setParent(null);
        children.remove(child);
    }

    public Long getId() {

        return id;
    }

    @Transactional
    public List<Child> getReadOnlyChildren() {

        return Collections.unmodifiableList(children);
    }
}
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import java.io.Serializable;

@Entity
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Child implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "id")
    private Parent parent;

    public Long getId() {

        return id;
    }

    public Parent getParent() {

        return parent;
    }

    /**
     * Only for usage in {@link Parent}
     */
    void setParent(final Parent parent) {

        this.parent = parent;
    }
}
import org.springframework.data.repository.CrudRepository;

public interface ParentRepository extends CrudRepository<Parent, Long> {}
import com.avaya.adw.db.repo.ParentRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;

@RestController
@RequestMapping("/api/v1.0/parents")
public class ParentController {

    private final String hostPath;

    private final ParentRepository parentRepository;

    public ParentController(@Value("${app.hostPath}") final String hostPath,
                          final ParentRepository parentRepository) {

        // in application.properties: app.hostPath=/api/v1.0/
        this.hostPath = hostPath; 
        this.parentRepository = parentRepository;
    }

    @CrossOrigin(origins = "*")
    @GetMapping("/{id}")
    public ResponseEntity<?> getParent(@PathVariable(value = "id") long id) {

        final Parent parent = parentRepository.findOne(id);
        if (parent == null) {
            return new ResponseEntity<>(new HttpHeaders(), HttpStatus.NOT_FOUND);
        }
        Link selfLink = linkTo(Parent.class)
                .slash(hostPath + "parents")
                .slash(parent.getId()).withRel("self");
        Link updateLink = linkTo(Parent.class)
                .slash(hostPath + "parents")
                .slash(parent.getId()).withRel("update");
        Link deleteLink = linkTo(Parent.class)
                .slash(hostPath + "parents")
                .slash(parent.getId()).withRel("delete");
        Link syncLink = linkTo(Parent.class)
                .slash(hostPath + "parents")
                .slash(parent.getId())
                .slash("sync").withRel("sync");
        parent.add(selfLink);
        parent.add(updateLink);
        parent.add(deleteLink);
        parent.add(syncLink);
        return new ResponseEntity<>(adDataSource, new HttpHeaders(), HttpStatus.OK);
    }
}

따라서 GET ... / api / v1.0 / parents / 1을 보내면 응답은 다음과 같습니다.

{
    "id": 1,
    "children": [
        {
            "id": 1,
            "parent": 1
        },
        {
            "id": 2,
            "parent": 1
        },
        {
            "id": 3,
            "parent": 1
        }
    ],
    "links": [
        {
            "rel": "self",
            "href": "http://.../api/v1.0/parents/1"
        },
        {
            "rel": "update",
            "href": "http://.../api/v1.0/parents/1"
        },
        {
            "rel": "delete",
            "href": "http://.../api/v1.0/parents/1"
        },
        {
            "rel": "sync",
            "href": "http://.../api/v1.0/parents/1/sync"
        }
    ]
}

그러나 데이터베이스에 실제 값을 가져 오지 않기 위해 자식을 포함하지 않거나 빈 배열 또는 null로 포함하지 않을 것으로 예상합니다.

구성 요소에는 다음과 같은 주목할만한 maven 종속성이 있습니다.

 - Spring Boot Starter 1.5.7.RELEASE
 - Spring Boot Starter Web 1.5.7.RELEASE (version from parent)
 - Spring HATEOAS 0.23.0.RELEASE
 - Jackson Databind 2.8.8 (it's 2.8.1 in web starter, I don't know why we overrode that)
 - Spring Boot Started Data JPA 1.5.7.RELEASE (version from parent) -- hibernate-core 5.0.12.Final

디버깅 결과 serialReset 중 parentRepository.findOne (id)의 Parent와 Parent.children의 다른 하나가 선택되었습니다.

처음에는 @JsonIgnore를 게으른 컬렉션에 적용하려고 시도했지만 실제로 뭔가가 이미 포함되어 있어도 컬렉션을 무시합니다.

나는 jackson-datatype-hibernate 프로젝트에 대해 알았다.

이것의 아이디어는 ObjectMapper에 Hibernate5Module (hibernate의 버전 5가 사용 된 경우)을 등록하는 것이며 모듈은 기본적으로 FORCE_LAZY_LOADING 설정이 false로 설정되어 있어야합니다.

따라서이 종속성 jackson-datatype-hibernate5, 버전 2.8.10 (부모의)을 포함했습니다. 그리고 Spring Boot에서 활성화하는 방법을 Google에서 찾았습니다 (다른 출처를 찾았지만 대부분 이것을 참조합니다).

1. 간단한 추가 모듈 (Spring Boot 전용) :

import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HibernateConfiguration {

    @Bean
    public Module disableForceLazyFetching() {

        return new Hibernate5Module();
    }
}

디버깅 결과, Spring이 Parent를 반환 할 때 호출되는 ObjectMapper는이 모듈을 포함하고 예상대로 강제 지연 설정을 false로 설정했습니다. 그러나 여전히 자식을 가져옵니다.

추가 디버깅 결과는 다음과 같습니다. com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields는 속성 (com.fasterxml.jackson.databind.ser.BeanPropertyWriter)을 반복하고 첫 번째 줄은 final Object value 인 메소드 serializeAsField를 호출합니다. = (_accessorMethod == null)? _field.get (bean) : _accessorMethod.invoke (bean); 게으른 로딩을 트리거합니다. 코드가 실제로 최대 절전 모드 모듈에 관심이있는 곳을 발견 할 수 없었습니다.

upd 또한 null이 아닌 게으른 속성의 실제 ID를 포함해야하는 SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS를 활성화하려고했습니다 (기본값).

@Bean
public Module disableForceLazyFetching() {

    Hibernate5Module module = new Hibernate5Module();
    module.enable(Hibernate5Module.Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS);

    return module;
}

디버깅 결과 옵션이 활성화되었지만 여전히 영향을 미치지 않는 것으로 나타났습니다.

2. Spring MVC에 모듈을 추가하도록 지시하십시오 :

import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.List;

@Configuration
@EnableWebMvc
public class HibernateConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
                .modulesToInstall(new Hibernate5Module());
        converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
    }
}

이것은 또한 호출중인 ObjectMapper에 모듈을 성공적으로 추가하지만 여전히 제 경우에는 영향을 미치지 않습니다.

3. ObjectMapper를 완전히 새로운 것으로 교체하십시오.

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class HibernateConfiguration {

    @Primary
    @Bean(name = "objectMapper")
    public ObjectMapper hibernateAwareObjectMapper(){

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new Hibernate5Module());

        return mapper;
    }
}

다시 말하지만, 모듈이 추가되었지만 효과가 없습니다.

이 모듈을 추가하는 다른 방법이 있지만 모듈이 추가되었으므로 실패하지 않습니다.

해결법

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

    1.가능한 해결책으로 Vlad Mihalcea는 jackson-datatype-hibernate 프로젝트에 신경 쓰지 않고 단순히 DTO를 구성한다고 제안합니다. 잭슨이 3 일 동안 10-15 시간 씩 3 일 동안 원하는 일을하도록 강요했지만 포기했습니다.

    가능한 해결책으로 Vlad Mihalcea는 jackson-datatype-hibernate 프로젝트에 신경 쓰지 않고 단순히 DTO를 구성한다고 제안합니다. 잭슨이 3 일 동안 10-15 시간 씩 3 일 동안 원하는 일을하도록 강요했지만 포기했습니다.

    EAGER 페칭이 일반적으로 나쁜 방법에 대한 Vlad의 블로그 게시물을 읽은 후 다른 측면에서 페치 원칙을 살펴 보았습니다. 이제 이해해야 할 것은 가져올 속성과 한 번만 가져올 속성을 시도하고 정의하는 것이 좋지 않다는 것입니다. 전체 애플리케이션 (@Basic 또는 @OneToMany 또는 @ManyToMany의 페치 주석 속성을 사용하여 엔티티 내부에 있음). 어떤 경우에는 추가 게으른 인출 또는 다른 경우에는 불필요한 열성 인출에 대한 페널티가 발생합니다. 즉, 각 GET 끝점에 대해 사용자 지정 쿼리와 DTO를 만들어야합니다. 그리고 DTO의 경우 JPA 관련 문제가 없으므로 데이터 유형 종속성을 제거 할 수 있습니다.

    코드 예제에서 볼 수 있듯이 편의상 JPA와 HATEOAS를 결합했습니다. "궁극적 인 부동산 가져 오기 선택"에 대한 이전 단락과 각 GET에 대해 DTO를 생성한다는 점을 고려하면 일반적으로 그렇게 나쁘지는 않지만 HATEOAS를 해당 DTO로 옮길 수 있습니다. 또한 JPA 엔터티가 ResourseSupport 클래스를 확장하지 못하도록하면 실제로 비즈니스 논리와 관련된 부모를 확장 할 수 있습니다.

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

    2.

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Child> children = new ArrayList<>();
    

    열심히 가져오고 싶지 않은 필드에 fetch 속성을 추가하십시오.

  3. from https://stackoverflow.com/questions/47989857/jackson-triggering-jpa-lazy-fetching-on-serialization by cc-by-sa and MIT license