Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jackson triggering JPA Lazy Fetching on serialization

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.

like image 573
Sam Avatar asked Dec 27 '17 09:12

Sam


1 Answers

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.

like image 164
Sam Avatar answered Oct 21 '22 13:10

Sam