杰克逊在序列化上触发JPA Lazy Fetching

时间:2017-12-27 09:48:31

标签: java json spring hibernate jpa

我们有一个后端组件,它通过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"
        }
    ]
}

但我希望它不包含children或包含它作为空数组或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

到目前为止已经尝试了

调试显示,在select Parent上有一个parentRepository.findOne(id),在序列化过程中Parent.children上有一个@JsonIgnore

首先,我尝试将Hibernate5Module应用于延迟集合,但即使它实际上包含某些内容(已经被提取),它也会忽略该集合。

我发现了声称

jackson-datatype-hibernate项目
  

构建Jackson模块(jar)以支持JSON序列化和   Hibernate(http://hibernate.org)特定数据类型的反序列化   和财产; 特别是延迟加载方面

这样做的想法是将ObjectMapper(如果使用了hibernate的第5版)注册到FORCE_LAZY_LOADING,并且应该这样做,因为模块的设置false设置为{ {1}}默认情况下。

所以,我包含了这个依赖jackson-datatype-hibernate5,版本2.8.10(来自父级)。并用Google搜索了enable it in Spring Boot(我也找到了其他来源,但他们大多指的是这个)。

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返回ObjectMapper时调用的Parent包含此模块,并且强制延迟设置设置为false,正如预期的那样。但它仍然提取children

进一步调试显示:com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields遍历属性(com.fasterxml.jackson.databind.ser.BeanPropertyWriter)并调用其方法serializeAsField,其中第一行是:final Object value = (_accessorMethod == null) ? _field.get(bean) : _accessorMethod.invoke(bean);,它触发延迟加载。我找不到代码实际上关心那个hibernate模块的任何地方。

upd 还尝试启用应包含惰性属性的实际ID的SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS,而不是null(这是默认值)。

@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;
    }
}

同样,我可以看到添加了模块但对我没有任何影响。

还有其他方法可以添加此模块,但由于添加了模块,因此我没有失败。

3 个答案:

答案 0 :(得分:1)

作为一种可能的解决方案,Vlad Mihalcea建议我不要为jackson-datatype-hibernate project而烦恼,只需构建一个DTO。我一直在努力迫使杰克逊每天10-15个小时做我想做的3天,但我已经放弃了。

在阅读弗拉德的博客文章后,我已经看过另一方面的原则 - {3}}一般情况 - 我现在明白了,这是一个不好的主意,尝试和定义要获取的属性以及不为整个应用程序仅提取一次的属性(使用fetch@Basic@OneToMany的{​​{1}}注释属性在实体内部。这将导致在某些情况下额外的延迟提取或在其他情况下不必要的急切提取的惩罚。也就是说,我们需要为每个GET端点创建一个自定义查询EAGER fetching is bad。对于DTO,我们不会遇到任何与JPA相关的问题,这也会让我们删除数据类型依赖性。

还有更多要讨论的内容:正如您在代码示例中所看到的,为方便起见,我们将JPA和HATEOAS结合在一起。虽然它总的来说并不坏,但考虑到前一段关于&#34;最终属性的选择&#34;并且我们为每个GET创建一个DTO,我们可以将HATEOAS移动到该DTO。此外,从扩展@ManyToMany类中释放JPA实体可以扩展实际与业务逻辑相关的父级。

答案 1 :(得分:0)

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

尝试将fetch属性添加到您不想急切获取的字段

答案 2 :(得分:0)

即使该属性设置为 LAZY,Spring Boot 也会在视图层打开一个事务,允许在需要时延迟加载数据。幸运的是,我们可以通过以下 Spring 属性关闭此行为:

spring.jpa.open-in-view=false