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")
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; } 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) {
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.