Jackson 在序列化时触发 JPA Lazy Fetching

Posted

技术标签:

【中文标题】Jackson 在序列化时触发 JPA Lazy Fetching【英文标题】:Jackson triggering JPA Lazy Fetching on serialization 【发布时间】:2018-06-07 23:04:39 【问题描述】:

我们有一个后端组件,通过 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 *** 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(来自父版本) - 春季 HATEOAS 0.23.0.RELEASE - Jackson Databind 2.8.8(它是 web starter 中的 2.8.1,我不知道我们为什么要覆盖它) - Spring Boot Started Data JPA 1.5.7.RELEASE(来自父版本)--hibernate-core 5.0.12.Final

目前尝试过

调试显示在序列化过程中有一个selectParent parentRepository.findOne(id) 和另一个Parent.children

起初,我尝试将@JsonIgnore 应用于惰性集合,但即使集合实际上包含某些内容(已被获取),也会忽略该集合。

我发现了jackson-datatype-hibernate 声称的项目

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

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

所以,我包含了这个依赖项jackson-datatype-hibernate5,版本 2.8.10(来自父级)。并用谷歌搜索了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 在返回 Parent 时调用的 ObjectMapper 包含此模块,并且按预期将强制延迟设置设置为 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); 触发延迟加载。我找不到代码真正关心该休眠模块的任何地方。

upd 还尝试启用 SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS,它应该包含惰性属性的实际 id,而不是 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;
    

再次,我可以看到模块已添加,但这对我没有任何影响。


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

【问题讨论】:

你在休眠模块上启用SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS功能了吗? @RobbyCornelissen 不,这有关系吗?我已经阅读了这方面的 javadoc,虽然null 没问题。我现在就试试这个! 是的,就是这样。我已经成功使用这个模块有一段时间了,但不得不承认我自己扩展了它,因为我也无法让它按照我想要的方式运行。 @RobbyCornelissen 我已启用该选项,但没有效果。也许@JsonIdentityInfo 与它有关?我将更新答案以显示我是如何做到的 发现Spring Boot uses transactions in view by default。所以,如果我禁用那些我在序列化过程中得到LazyInitializationException 【参考方案1】:

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

spring.jpa.open-in-view=false

【讨论】:

【参考方案2】:
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Child> children = new ArrayList<>();

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

【讨论】:

但在@OneToMany上默认是惰性的 你是对的,我的错。我认为杰克逊默认包含所有字段,然后触发延迟提取,我认为您应该将其添加到您的字段中:@JsonInclude(JsonInclude.Include.NON_EMPTY) Jackson 需要致电children.isEmpty() 以了解其是否为空。所以,这也应该触发获取。虽然,在调试器中,它仍然会转到相同的 serializeAsField 并在那里访问它【参考方案3】:

作为一种可能的解决方案,Vlad Mihalcea 建议我不要理会jackson-datatype-hibernate project,而只需构建一个 DTO。我一直试图强迫杰克逊做我想做的事,持续 3 天,每次 10-15 小时,但我放弃了。

在阅读了 Vlad 的博客文章(关于 EAGER fetching is bad 的一般情况)后,我从另一个方面研究了获取原则——我现在明白了,尝试定义要获取的属性和不获取的属性是一个坏主意整个应用程序一次(即在使用@Basic@OneToMany@ManyToManyfetch 注释属性的实体内部)。这将导致在某些情况下额外的延迟获取或在其他情况下不必要的急切获取受到惩罚。也就是说,我们需要为每个 GET 端点创建一个自定义查询 and a DTO。对于 DTO,我们不会遇到任何与 JPA 相关的问题,这也将让我们移除数据类型依赖。

还有更多要讨论的内容:正如您在代码示例中看到的,为了方便起见,我们将 JPA 和 HATEOAS 耦合在一起。虽然总体上并没有那么糟糕,但考虑到前面关于“最终属性获取选择”的段落以及我们为每个 GET 创建一个 DTO,我们可能会将 HATEOAS 移至该 DTO。此外,从扩展 ResourseSupport 类中释放 JPA 实体可以让它扩展实际上与业务逻辑相关的父级。

【讨论】:

以上是关于Jackson 在序列化时触发 JPA Lazy Fetching的主要内容,如果未能解决你的问题,请参考以下文章

JPA / Jackson - 反序列化时排除字段并在序列化时包含它们

Jackson 未对 JPA id 字段进行序列化

JPA 懒加载问题

jpa oneToMany和jackson序列化问题(新手)

避免对未获取的惰性对象进行 Jackson 序列化

使用杰克逊将双向 JPA 实体序列化为 JSON