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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With