Jackson launches serialization JPA Lazy Fetching

We have an internal component that provides access to database data (PostgreSQL) through JPA in the RESTful API.

The problem is that when I submit the JPA entity as a REST response, I see Jackson launch all Lazy JPA relationships.


Sample code (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 answer will be as follows:

 { "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 that it will not contain children or contain it as an empty array or a null value - so as not to get 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 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 during serialization, there is one select for Parent in parentRepository.findOne(id) and another for Parent.children .

At first I tried to apply @JsonIgnore to lazy collections, but this ignores the collection, even if it really contains something (has already been extracted).

I found out about the Jackson-Datatype-Hibernate project , which claims to be

Build the Jackson module (jar) to support JSON serialization and deserialization of certain Hibernate data types and properties ( http://hibernate.org ); especially lazy aspects .

The idea behind this is to register the Hibernate5Module (if version 5 hibernate is used) with ObjectMapper and this should be done because the module's FORCE_LAZY_LOADING parameter is set to false by default.

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


1. Simple add-on module (specific for 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(); } } 

Debugging showed that the ObjectMapper that Spring invokes when it returns, Parent contains this module, and the force lazy parameter is set to false, as expected. But then it still attracts children .

Further debugging showed: com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields over the properties ( com.fasterxml.jackson.databind.ser.BeanPropertyWriter ) and calls their serializeAsField method, where the first line is: final Object value = (_accessorMethod == null)? _field.get(bean): _accessorMethod.invoke(bean); final Object value = (_accessorMethod == null)? _field.get(bean): _accessorMethod.invoke(bean); final Object value = (_accessorMethod == null)? _field.get(bean): _accessorMethod.invoke(bean); which causes lazy loading. I could not find a place where the code really cared about this hibernation module.

upd Also tried to include SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS which should include the actual id of the lazy property, not null (by 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 it still did not work.


2. Have Spring MVC add the 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())); } } 

It also successfully adds a module to ObjectMapper , which is called, but it still has no effect in my case.


3. Replace ObjectMapper completely 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 see that the module has been added, but this has no effect for me.


There are other ways to add this module, but I'm not mistaken, since the module has been added.

+8
source share
2 answers

As a possible solution, Vlad Mikhalcha assumes that I am not worried about the jackson-datatype-hibernate project and just construct the DTO. I tried to get Jackson to do what I want for 3 days for 10-15 hours each, but I refused.

I looked at the filing principle on the other hand after reading Vlad’s post about how EAGER fetching is bad in general - I now understand that it’s a bad idea to try to determine which property to choose and what not to retrieve only once for the entire application (then is inside the object using the fetch attribute of the @Basic or @OneToMany or @ManyToMany annotation attribute). This will result in a fine for additional lazy samples in some cases or unnecessary desire to receive in others. However, we need to create a custom query and DTO for each GET endpoint. And for the DTO, we will not have any problems with JPA, which will also allow us to remove the data type dependency.

You can still discuss: as you can see in the code example, we combine JPA and HATEOAS for convenience. Although overall this is not so bad, given the previous paragraph on the “final selection of properties”, and that we create a DTO for each GET, we can move HATEOAS to this DTO. In addition, freeing the JPA object from extending the ResourseSupport class allows you to extend the parent element, which is really relevant to business logic.

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

Just try adding the fetch property to the field that you don’t want to look forward to

0
source

Source: https://habr.com/ru/post/1274342/


All Articles