杰克逊在序列化时触发JPA延迟获取 [英] Jackson triggering JPA Lazy Fetching on serialization

查看:57
本文介绍了杰克逊在序列化时触发JPA延迟获取的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我们有一个后端组件,该组件通过JPA向RESTful API公开数据库(PostgreSQL)数据.

问题在于,当发送JPA实体作为REST响应时,我可以看到Jackson触发了所有懒惰的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


到目前为止已经尝试

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

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

我发现了有关声称拥有

jackson-datatype-hibernate 项目的信息>

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

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

因此,我包括了此依赖项jackson-datatype-hibernate5,版本2.8.10(来自父级).并搜索了 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,其中应包含lazy属性的实际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;
    }
}

同样,我可以看到该模块已添加,但这对我没有任何作用.


还有其他添加此模块的方法,但是我不会失败,因为添加了模块.

解决方案

作为可能的解决方法,弗拉德·米哈尔西(Vlad Mihalcea) 建议我不要理会jackson-datatype-hibernate project,而只是构造一个DTO.我一直在强迫杰克逊每次做10天到15个小时的3天,但是我已经放弃了.

在阅读了弗拉德(Vlad)的博客文章后,我从另一个角度审视了获取原理,该文章关于和一个DTO .对于DTO,我们不会遇到任何与JPA相关的问题,这也将使我们消除数据类型的依赖性.

还有更多要讨论的内容:如您在代码示例中所看到的,为了方便起见,我们将JPA和HATEOAS结合在一起.尽管总体上来说还不错,但考虑到上一段有关最终属性获取选择"的内容,并为每个GET创建一个DTO,我们可以将HATEOAS移至该DTO.另外,从扩展ResourseSupport类释放JPA实体,还可以扩展实际上与业务逻辑相关的父级.

We have a back-end component that exposes database (PostgreSQL) data via JPA to RESTful API.

The problem is that when sending a JPA entity as a REST response, I can see Jackson triggering all Lazy JPA relationships.


Code example (simplified):

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


So, if I send GET .../api/v1.0/parents/1, the response is the following:

{
    "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"
        }
    ]
}

But I expect it to not contain children or contain it as an empty array or null -- to not fetch the actual values from the database.


The component has the following notable maven dependencies:

 - 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


Tried so far

Debugging showed that there is one select on Parent at parentRepository.findOne(id) and another one on Parent.children during serialization.

At first, I tried applying @JsonIgnore to lazy collections, but that ignores the collection even if it actually contains something (has already been fetched).

I found out about jackson-datatype-hibernate project that claims to

build Jackson module (jar) to support JSON serialization and deserialization of Hibernate (http://hibernate.org) specific datatypes and properties; especially lazy-loading aspects.

The idea of this is to register Hibernate5Module (if version 5 of hibernate is used) to the ObjectMapper and that should do it as the module has a setting FORCE_LAZY_LOADING set to false by default.

So, I included this dependency jackson-datatype-hibernate5, version 2.8.10 (from parent). And googled a way to enable it in Spring Boot (I also found other sources but they mostly refer to this).


1. Straightforward add module (Spring Boot specific):

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();
    }
}

Debugging showed, that ObjectMapper that is being invoked by Spring when it returnes Parent contains this module and has force lazy setting set to false just as expected. But then it still fetches children.

Further debugging showed: com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields iterates through properties (com.fasterxml.jackson.databind.ser.BeanPropertyWriter) and calls their method serializeAsField, where the first line is: final Object value = (_accessorMethod == null) ? _field.get(bean) : _accessorMethod.invoke(bean); which is triggering lazy loading. I could not spot any place where the code would actualy care about that hibernate module.

upd Also tried enabling SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS that should include actual id of the lazy property, rather than null (which is the default).

@Bean
public Module disableForceLazyFetching() {

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

    return module;
}

Debugging showed that the option is enabled but that still had no effect.


2. Instruct Spring MVC to add a module:

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()));
    }
}

This also successfully adds the module to the ObjectMapper that is being invoked, but it still has no effect in my case.


3. Completely replace ObjectMapper with a new one:

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

Again, I can see the module is added but that has no effect for me.


There are other ways to add this module, but I am not failing in this, since the module is added.

解决方案

As a possible resolution, Vlad Mihalcea suggests that I don't bother with jackson-datatype-hibernate project and simply construct a DTO. I've been trying to force Jackson to do what I want for 3 days 10-15 hours each, but I've given up.

I've looked at fetching principle from another side after reading Vlad's blog post on how EAGER fetching is bad in general -- I now understand, that it's a bad idea to try and define what property to fetch and what not to fetch only once for the whole application (that is inside the entity using fetch annotation attribute of @Basic or of @OneToMany or @ManyToMany). That will cause a penalty for additional lazy fetching in some cases or unnecessary eager fetching in other ones. That said, we need to create a custom query and a DTO for each GET endpoint. And for DTO we won't have any of those JPA related issues, which will also let us remove the datatype dependency.

There is more to discuss: as you can see in the code example, we couple JPA and HATEOAS together for convenience. While it's not that bad in general, considering the previous paragraph about "the ultimate property fetching choice" and that we create a DTO for each GET, we may move HATEOAS to that DTO. Also, releasing a JPA entity from extending a ResourseSupport class lets it extend a parent that is actually relevant to the business logic.

这篇关于杰克逊在序列化时触发JPA延迟获取的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆