[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.가능한 해결책으로 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.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Child> children = new ArrayList<>();
열심히 가져오고 싶지 않은 필드에 fetch 속성을 추가하십시오.
from https://stackoverflow.com/questions/47989857/jackson-triggering-jpa-lazy-fetching-on-serialization by cc-by-sa and MIT license
'SPRING' 카테고리의 다른 글
[SPRING] @ControllerAdvice로 간단한 서블릿 필터 작동 (0) | 2019.08.14 |
---|---|
[SPRING] Spring Webflux,`ServerResponse` 테스트 (0) | 2019.08.14 |
[SPRING] 경로 변수를 기반으로하는 스프링 보안 권한 (0) | 2019.08.13 |
[SPRING] 스프링 부트는 자바 주석에서 구성 속성 / 메시지를 외부화합니다. (0) | 2019.08.13 |
[SPRING] 프로그래밍 방식으로 Bean 검색 (0) | 2019.08.13 |