Could not write JSON: failed to lazily initialize a collection of role
Asked Answered
M

10

18

I tried to implement a REST service with Java, Hibernate, and Spring, which returns JSON.

I have map a many to many relation. I have a supplier that has a list of ingredients, and each ingredient has a list of suppliers.

I created the table:

CREATE TABLE supplier_ingredient (
  supplier_id BIGINT,
  ingredient_id BIGINT
)


ALTER TABLE supplier_ingredient ADD CONSTRAINT supplier_ingredient_pkey 
PRIMARY KEY(supplier_id, ingredient_id);

ALTER TABLE supplier_ingredient ADD CONSTRAINT 
fk_supplier_ingredient_ingredient_id FOREIGN KEY (ingredient_id) 
REFERENCES ingredient(id);

ALTER TABLE supplier_ingredient ADD CONSTRAINT 
fk_supplier_ingredient_supplier_id FOREIGN KEY (supplier_id) REFERENCES 
supplier(id);

Then I have an Ingredient model:

.....
.....
@ManyToMany(mappedBy = "ingredients")
@OrderBy("created DESC")
@BatchSize(size = 1000)
private List<Supplier> suppliers = new ArrayList<>();
....
....

Then I have a Supplier model:

....
@ManyToMany
@JoinTable( name = "supplier_ingredient ", 
        joinColumns = @JoinColumn(name = "supplier_id", referencedColumnName = "id"), 
        inverseJoinColumns = @JoinColumn(name = "ingredient_id", referencedColumnName = "id"), 
        foreignKey = @ForeignKey(name = "fk_supplier_ingredient_supplier_id"))
@OrderBy("created DESC")
@Cascade(CascadeType.SAVE_UPDATE)
@BatchSize(size = 1000)
private List<Ingredient> ingredients = new ArrayList<>();
....

Endpoint:

@RequestMapping(value = "/{supplierId:[0-9]+}", method = RequestMethod.GET)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public SupplierObject get(@PathVariable Long supplierId) {

    Supplier supplier = supplierService.get(supplierId);

    SupplierObject supplierObject = new SupplierObject (supplier);

    return SupplierObject;

}

Service

....
public Supplier get(Long supplierId) {

    Supplier supplier = supplierDao.getById(supplierId); (it does entityManager.find(entityClass, id))

    if (supplier == null) throw new ResourceNotFound("supplier", supplierId);

    return supplier;
}
....

SupplierObject

    @JsonIgnoreProperties(ignoreUnknown = true)
    public class SupplierObject extends IdAbstractObject {

    public String email;

    public String phoneNumber;

    public String address;

    public String responsible;

    public String companyName;

    public String vat;

    public List<Ingredient> ingredients = new ArrayList<>();

    public SupplierObject () {

    }

    public SupplierObject (Supplier supplier) {

        id = supplier.getId();
        email = supplier.getEmail();
        responsible = supplier.getResponsible();
        companyName = supplier.getCompanyName();
        phoneNumber = supplier.getPhone_number();
        ingredients = supplier.getIngredients();
        vat = supplier.getVat();
        address = supplier.getAddress();


    }
}

And IdAbstractObject

public abstract class IdAbstractObject{

    public Long id;

}

My problem is, when I call the endpoint:

http://localhost:8080/supplier/1

I got an error:

"Could not write JSON: failed to lazily initialize a collection of role: myPackage.ingredient.Ingredient.suppliers, could not initialize proxy - no Session; nested exception is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role: myPackage.ingredient.Ingredient.suppliers, could not initialize proxy

  • no Session (through reference chain: myPackage.supplier.SupplierObject["ingredients"]->org.hibernate.collection.internal.PersistentBag[0]->myPackage.ingredient.Ingredient["suppliers"])"

I followed this:

Avoid Jackson serialization on non fetched lazy objects

Now I haven't the error but in json returned, the ingredients field is null:

{
  "id": 1,
  "email": "[email protected]",
  "phoneNumber": null,
  "address": null,
  "responsible": null,
  "companyName": "Company name",
  "vat": "vat number",
  "ingredients": null
}

but in debug I can see ingredients....

enter image description here

Moraine answered 5/1, 2018 at 15:56 Comment(0)
D
15

This is the normal behaviour of Hibernate and Jackson Marshaller Basically you want to have the following: a JSON with all Supplier object details... included the Ingredients.

Please note that in this case you must be very carefull because you can have a cyclic reference when you try to create the JSON itself so you should use also JsonIgnore annotation

The first thing you must do is to load the Supplier and all its details (ingredients included).

How can you do it? By using several strategies... let's use the Hibernate.initialize. This must be used before the closing of hibernate session that is in the DAO (or repository) implementation (basically where you use the hibernate session).

So in this case (I assume to use Hibernate) in my repository class I should write something like this:

public Supplier findByKey(Long id)
{
    Supplier result = (Supplier) getSession().find(Supplier.class, id);
    Hibernate.initialize(result.getIngredients());
    return result;
}

Now you have the Supplier object with all its own details (Ingredients too) Now in your service you can do what you did that is:

@RequestMapping(value = "/{supplierId:[0-9]+}", method = RequestMethod.GET)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public SupplierObject get(@PathVariable Long supplierId) 
{
    Supplier supplier = supplierService.get(supplierId);
    SupplierObject supplierObject = new SupplierObject (supplier);
    return SupplierObject;
}

In this way Jackson is able in writing the JSON but let's give a look to the Ingredient object.. it has the following property:

@ManyToMany(mappedBy = "ingredients")
@OrderBy("created DESC")
@BatchSize(size = 1000)
private List<Supplier> suppliers = new ArrayList<>();

What will happen when Jackson tries to create the JSON? It will access to the each element inside the List<Ingredient> and it will try to create a JSON for this one too.... also for the suppliers list and this is a cyclic reference... so you must avoid it and you can avoid it by using the JsonIgnore annotation. For example you may write your Ingredient entity class in this way:

@JsonIgnoreProperties(value= {"suppliers"})
public class Ingredient implements Serializable
{
......
}

In this way you:

  • load the supplier object with all the related ingredient
  • avoid a cyclic reference when you try to create the JSON itself

In any case I would suggest to you to create specific DTO (or VO) object to use for marshalling and unmarshalling JSONs

I hope this is usefull

Angelo

Dosi answered 8/1, 2018 at 14:17 Comment(0)
F
16

You have some solutions to resolve this issue:

  1. You can use @ManyToMany(fetch = FetchType.EAGER)

But EAGER fetching is very bad from a performance perspective. Moreover, once you have an EAGER association, there is no way you can make it LAZY.

  1. You can use @ManyToMany @Fetch(FetchMode.JOIN)

More information: https://docs.jboss.org/hibernate/orm/3.2/api/org/hibernate/FetchMode.html

Edit: It can occur when you have the following line in yout application.properties file:

spring.jpa.open-in-view = false
Futtock answered 14/10, 2019 at 8:53 Comment(2)
changing: spring.jpa.open-in-view = true did the trick for me ...Aluino
spring.jpa.open-in-view = false My case.Muse
D
15

This is the normal behaviour of Hibernate and Jackson Marshaller Basically you want to have the following: a JSON with all Supplier object details... included the Ingredients.

Please note that in this case you must be very carefull because you can have a cyclic reference when you try to create the JSON itself so you should use also JsonIgnore annotation

The first thing you must do is to load the Supplier and all its details (ingredients included).

How can you do it? By using several strategies... let's use the Hibernate.initialize. This must be used before the closing of hibernate session that is in the DAO (or repository) implementation (basically where you use the hibernate session).

So in this case (I assume to use Hibernate) in my repository class I should write something like this:

public Supplier findByKey(Long id)
{
    Supplier result = (Supplier) getSession().find(Supplier.class, id);
    Hibernate.initialize(result.getIngredients());
    return result;
}

Now you have the Supplier object with all its own details (Ingredients too) Now in your service you can do what you did that is:

@RequestMapping(value = "/{supplierId:[0-9]+}", method = RequestMethod.GET)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public SupplierObject get(@PathVariable Long supplierId) 
{
    Supplier supplier = supplierService.get(supplierId);
    SupplierObject supplierObject = new SupplierObject (supplier);
    return SupplierObject;
}

In this way Jackson is able in writing the JSON but let's give a look to the Ingredient object.. it has the following property:

@ManyToMany(mappedBy = "ingredients")
@OrderBy("created DESC")
@BatchSize(size = 1000)
private List<Supplier> suppliers = new ArrayList<>();

What will happen when Jackson tries to create the JSON? It will access to the each element inside the List<Ingredient> and it will try to create a JSON for this one too.... also for the suppliers list and this is a cyclic reference... so you must avoid it and you can avoid it by using the JsonIgnore annotation. For example you may write your Ingredient entity class in this way:

@JsonIgnoreProperties(value= {"suppliers"})
public class Ingredient implements Serializable
{
......
}

In this way you:

  • load the supplier object with all the related ingredient
  • avoid a cyclic reference when you try to create the JSON itself

In any case I would suggest to you to create specific DTO (or VO) object to use for marshalling and unmarshalling JSONs

I hope this is usefull

Angelo

Dosi answered 8/1, 2018 at 14:17 Comment(0)
W
3

In my project I came across the same problem as yours. The problem is that by the time of reading the data "one to many" the session has already been closed. To get all the data, you need to explicitly initialize or use the transaction. I used an explicit initialization. You need to add a line in the DAO:

Hibernate.initialize(supplier.getIngredients());

After that, Hibernate will load all the data from the database. To avoid generating an exception when serializing to JSON, I add the @JsonIgnore annotation in the one-to-many model field.

Here is an example of my code:

1.Model

@OneToMany(mappedBy = "commandByEv", fetch = FetchType.LAZY)
@JsonIgnore
private Set<Evaluation> evaluations;

2. DAO

public Command getCommand(long id) {
Session session = sessionFactory.getCurrentSession();
Evaluation evaluation = session.get(Evaluation.class, id);
Hibernate.initialize(evaluation.getCommand());
return evaluation.getCommand();
}
Woodham answered 5/1, 2018 at 17:17 Comment(1)
My relation is manytomany, anyway, I don't understand your advice. first you say You need to add a line in the DAO: Hibernate.initialize(supplier.getIngredients()); then you write a DAO ... I'm confuseMoraine
A
3

Just add @JsonIgnore after @oneToMany in your model class.

Arius answered 13/9, 2018 at 14:45 Comment(2)
Does not work in my case.Astonishing
it does not work because the direction of using @JsonIgnore is not specified, we should add it in the child class of the relationship, to break the cyclic barrier.Hereinto
B
3

You should use a jackson-datatype-hibernate.

https://github.com/FasterXML/jackson-datatype-hibernate

And add this on your Application.java

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new Hibernate5Module());
    return objectMapper;
}
Borneol answered 23/8, 2021 at 2:57 Comment(0)
M
0

This is due to the hibernate session closed before the lazy initialization kicked in.

The solution is explained well at this answer below. Avoid Jackson serialization on non fetched lazy objects

Margaret answered 5/1, 2018 at 16:13 Comment(1)
So, I tried like the example you linked, but now the ingredients in the returned json are null: { "id": 1, "email": "[email protected]", "phoneNumber": null, "address": null, "responsible": null, "companyName": "123123", "vat": "ddstere", "ingredients": null } I am using Hibernate 5 and for that, I am using Hibernate5Module instead of Hibernate4Module, like in your link I am sure, I saw in debug, that the ingredients are presentMoraine
W
0

In my application.properties file open-in-view property of spring jpa was false. I had to comment to get rid of this.

 #spring.jpa.open-in-view=false

Hope this help somebody.

Watercool answered 19/1, 2022 at 16:18 Comment(1)
very bad pratice to enable thatTeodoor
T
0

I had to add spring.jpa.open-in-view = true to application.properties

Tertial answered 25/7, 2022 at 13:57 Comment(0)
H
0

See this issue comes, when we create a circular infinite loop. It comes when we try to establish relationship.

@OneToMany or @ManyToMany or @ManyToOne from both directions i.e parent to child.

There could be several solutions to the problem.

I find the simplest one as to put

@JsonIgnore on the child child, which will break the loop to read the parent class again, and all data will come up efficiently !!!

thank you.

Hereinto answered 10/8, 2023 at 13:41 Comment(0)
U
0

Adding spring.jpa.open-in-view = true may solve the problem but is not advised for MVC application.

Usually, an MVC application works like this :

HTTP Request 
       |
        -> Controller 
                |   
                 -> Service 
                       |
                        -> Repository 
                               |
                    Service <-
                       |
          Controller <-
               |               
HTTP Response <- 

Hibernate session is opened on the Service layer and closed once you return to the Controller.

Setting spring.jpa.open-in-view = true will open the session on the Controller layer and can therefore cause other issues you didn't have before.

This variable was originally created to handle sessions this way for application that use server templating (like thymeleaf or jsp for example).

To avoid the failed to lazily initialize a collection error without changing spring.jpa.open-in-view to true, try moving the code that is probably in your controller method to the service. If that is not an option, you can also open the session in your controller method. This will do the same than setting the variable to true but only for this method which is better.

You can find more info here : https://www.baeldung.com/spring-open-session-in-view

Upstate answered 13/3 at 9:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.