OneToMany relationship causing infinite loop using Spring Data JPA with hibernate as provider
The problem here is not the type of exception but the infinite loop that causes this exception
I tried #JsonIgnoreProperties which gives me another error => 'Could not write JSON: No serializer found for class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer and no properties discovered to create BeanSerializer'
The post referencing the solution does not have a solution that adresses my problem.
One says use #JsonManagedReference and #JsonBackReference that does stop the recursion but excludes the object (UserGroup in 'myUser' entity) from the result which I need when I want an object of 'myUser' entity.
The other one says about overriding ToString method which I don't do.
Another one explains why there is an infinite loop and suggest as solution to not do that way. I quote "Try to create DTO or Value Object (simple POJO) without cycles from returned model and then return it."
And this one Difference between #JsonIgnore and #JsonBackReference, #JsonManagedReference explains the difference but doing so I will have the same problem as the first one
'myUser' entity
#Entity
public class MyUser {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
private Integer age;
//#JsonIgnoreProperties({"myUsers"})
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "userGroupId")
private UserGroup userGroup;
'UserGroup' entity
#Entity
public class UserGroup {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Integer groupOrder;
#OneToMany
(
mappedBy = "userGroup",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<MyUser> myUsers;
change the getUserGroup() method in your MyUser class as follows.
#Entity
public class MyUser
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
private Integer age;
//#JsonIgnoreProperties({"myUsers"})
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "userGroupId")
private UserGroup userGroup;
public UserGroup getUserGroup()
{
userGroup.setMyUsers(null);
return userGroup;
}
}
you need to add #JsonIgnore annotation at #OneToMany
like this
#JsonIgnore
#OneToMany
(
mappedBy = "userGroup",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<MyUser> myUsers;
I think I'm getting the point of your problem. You want to fetch MyUser including the userGroup data without the circular reference.
Based from the solutions you enumerated, I suggest you should still use the #JsonBackReference and #JsonManagedReference to prevent recursion on your entities and for the solution on your problem, you can try to use a mapper (MapStruck) and map the userGroup details to a DTO during the retrieval of data from the service.
DTOs:
public class MyUserDto {
private Long id;
private String firstName;
private String lastName;
private String email;
private Integer age;
private UserGroupDto userGroupDto;
}
public class UserGroupDto {
private Long id;
private Integer groupOrder;
}
Mapper (MapStruck):
#Mapper(componentModel = "spring")
public interface MyUserMapper {
MyUserMapper INSTANCE = Mappers.getMapper(MyUserMapper.class);
UserGroupDto userGroupToDto(UserGroup userGroup);
#Mapping(source = "myUser.userGroup", target = "userGroupDto")
MyUserDto myUserToDto(MyUser myUser);
}
After retrieving the data from your repository, you may then call the myUserToDto method to map the entity to a DTO.
This is just one way of solving your problem.
Related
I'm fairly new to spring and hibernate and I'm creating a simple application that has two entity classes that are linked by #OneToMany and #ManyToOne relationships.
#Entity
public class ActiveIngredient {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id")
private int id;
#Column(name="name")
private String name;
#Column(name="active")
private String active;
#Column(name="potency")
private double potency;
#ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE,
CascadeType.DETACH, CascadeType.REFRESH})
#JoinColumn(name="manufacturer")
private Manufacturer manufacturer;
and
#Entity
public class Manufacturer {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="id")
private int manId;
#Column(name="name")
private String manName;
#Column(name="country")
private String country;
#Column(name="city")
private String city;
#Column(name="email")
private String email;
#Column(name="phone")
private String phone;
#OneToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE,
CascadeType.DETACH, CascadeType.REFRESH}, mappedBy = "manufacturer")
private List<ActiveIngredient> activeIngredients;
#OneToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE,
CascadeType.DETACH, CascadeType.REFRESH}, mappedBy = "manufacturer")
private List<ExcipientIngredient>excipientIngredients;
On the front end, a thymeleaf template is used where users can submit the data to create an ingredient and manufacturer. Below is the controller:
#Controller
#RequestMapping("/excipients")
public class ExcipientIngredientController {
#Autowired
private ExcipientIngredientService excipientIngredientService;
#Autowired
private ManufacturerService manufacturerService;
#GetMapping("/listExc")
public String listExcipients(Model model) {
List<ExcipientIngredient> theExcipient = excipientIngredientService.findAll();
//add list to the model.
model.addAttribute("excipient", theExcipient);
//return the thymeleaf page with this path.
return "list-excipients";
}
#GetMapping("/addExcipientForm")
public String addForm(Model model){
//to add a new excipient ingredient, need to create a new object
ExcipientIngredient excipientIngredient = new ExcipientIngredient();
Manufacturer manufacturer = new Manufacturer();
//add to the model
model.addAttribute("excipientIngredient", excipientIngredient);
model.addAttribute("manufacturer", manufacturer);
return "add-excipient-form";
}
#PostMapping("/saveExc")
public String saveExcipients(#ModelAttribute("excipientIngredient") ExcipientIngredient excipientIngredient, #ModelAttribute("manufacturer") Manufacturer manufacturer) {
// save the Ingredient
excipientIngredientService.saveOrUpdate(excipientIngredient);
manufacturerService.saveOrUpdate(manufacturer);
// use a redirect to prevent duplicate submissions
return "redirect:/excipients/listExc";
}
}
and this is the implementation for the save/update method.
#Override
public void saveOrUpdate(ExcipientIngredient excipientIngredient) {
Session currentSession = entityManager.unwrap(Session.class);
currentSession.saveOrUpdate(excipientIngredient);
}
Everything works fine when creating an ingredient and being updated into MySQL database, however, when the manufacturer is added to the database, an extra record is created that is completely null:
MySQL entry
I've been trying for a few hours to resolve this issue but have had no luck. Any suggestions or pointing me in the right direction would be appreciated.
In the Manufacturer class, you can try removing the cascade parameter from the #OneToMany annotation.
Here's a good article that you might find useful: https://www.baeldung.com/hibernate-one-to-many
There are two problems that I do not understand.
First, the error message on the console. It does not give me the whole error message. Therefore I do not understand the issue at all :S The IDE is STS.
Second, why do I get this error, "JsonMapping failed to lazily initialize a..."
#Test
public void testUpdateCar() throws Exception {
Car car = carRepository.findById(new Long(1)).get();
car.setBrand("Mazda");
car.setModel("326");
String putJSON = objectMapper.writeValueAsString(car);
mockMVC.perform(MockMvcRequestBuilders.put(String.format("/api/cars/%d", car.getId())).contentType(MediaType.APPLICATION_JSON_UTF8).content(putJSON))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(MockMvcResultMatchers.content().contentType("application/hal+json;charset=UTF-8"));
}
Car:
#Entity
public class Car {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToOne
private User owner;
private String brand;
private String model;
private String color;
private String plate;
private String additionals;
Update 1:
The error itself:
com.fasterxml.jackson.databind.JsonMappingException: failed to lazily
initialize a collection of role:
me.eraytuncer.carpool.entity.User.carList, could not initialize proxy
- no Session (through reference chain: me.eraytuncer.carpool.entity.Car["owner"]->me.eraytuncer.carpool.entity.User["carList"])
Update 2:
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String phone;
private String email;
#OneToMany(cascade = CascadeType.REMOVE, mappedBy = "owner")
private List<Car> carList;
Update 3:
#PutMapping("/cars/{id}")
ResponseEntity<?> replaceCar(#RequestBody Car newCar, #PathVariable Long id) {
if (repository.existsById(id)) {
newCar.setId(id);
Resource<Car> carResource = assembler.toResource(repository.save(newCar));
try {
return ResponseEntity
.created(new URI(carResource.getId().expand().getHref()))
.body(carResource);
} catch (URISyntaxException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
} else {
throw new CarNotFoundException(id);
}
}
Update 4:
Adding #JsonIgnore solved the issue somehow. Maybe it was a misleading issue caused by infinite recursion?
Looks like field
private List<Car> carList;
is resolved lazily (default fetch type in #OneToMany), which means it is populated from DB only when getter for this field is called. In your case it is called by Jackson serializer outside scope of the Hibernate session and property cannot be populated. Changing fetch type to EAGER in #OneToMany on carList property should help.
Also consider using DTO pattern, because returning entities from API is considered as bad practice.
You have mapped #OneToOne against #OneToMany. It should be #ManyToOne on the owning side:
#ManyToOne
#JoinColumn(name = "user_id") // if needed
private User owner;
I faced the same issue and it got resolved by using #Transactional on the method. #Transactional help to keep open the session so that lazy collection could be fetched.
I am using Spring Data and I am in doubt why even after declaring foreign entities as Lazy loaded they are getting eagerly loaded for this method:
findByReportingManager_IdAndAndLevel(Long reporterManagerId, Integer level)
On logs, I can see the query as:
select userhierar0_.id as id1_28_,
userhierar0_.LEVEL as LEVEL5_28_,
userhierar0_.REPORTING_MANAGER_ID as REPORTIN9_28_,
userhierar0_.USER_ID as USER_ID10_28_
from USER_HIERARCHY userhierar0_
left outer join
USER_V3 user1_ on userhierar0_.REPORTING_MANAGER_ID=user1_.id
where user1_.id=? and userhierar0_.CUSTOMER_ID=? and userhierar0_.LEVEL=?
why extra join even if I am passing reporting manager id ?
UserHierarchy Class:
#Entity
#Table(name = "USER_HIERARCHY")
#JsonInclude(JsonInclude.Include.NON_EMPTY)
public class UserHierarchy {
#Id
#GeneratedValue
private Long id;
#ManyToOne(fetch = FetchType.LAZY) // LAZY LOADING
#JoinColumn(name = "USER_ID",referencedColumnName = "ID")
private User user;
#ManyToOne(fetch = FetchType.LAZY) //LAZY LOADING
#JoinColumn(name = "REPORTING_MANAGER_ID", referencedColumnName = "ID")
private User reportingManager;
#Column(name = "LEVEL")
private Integer level;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public User getReportingManager() {
return reportingManager;
}
public void setReportingManager(User reportingManager) {
this.reportingManager = reportingManager;
}
public Integer getLevel() {
return level;
}
public void setLevel(Integer level) {
this.level = level;
}
#Override
public String toString() {
return ReflectionToStringBuilder.toStringExclude(this, "user", "reportingManager");
}
User Entity class
#Entity
#Table(name = "USER")
public class User {
#Id
#GeneratedValue
private Long id;
#Column(name = "EMAIL")
private String email;
#Column(name = "CUSTOMER_ID")
private Long customerId;
#Column(name = "STATUS")
private String status;
// Getter and Setter
As per Spring's doc:
At query creation time you already make sure that the parsed property
is a property of the managed domain class.
So does that mean in order to make User object in "managed state" it uses join or I am wrong in the implementation ?
I stumbled across the same problem recently and it seems that there is no solution for this at the moment in Spring Data.
However I've created a ticket for it.
If you go with Criteria API or JPQL for this particular query, then it will work properly.
I have One-To-Many relationship, here is my code
#Entity
#Table(name = "catalog")
public class Catalog {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "catalog_id")
private int catalog_id;
#NotEmpty
#Size(min = 3, max = 255)
#Column(name = "name", nullable = false)
private String name;
#OneToMany(mappedBy="mycatalogorder")
private List<Order> orders;
#OneToMany(mappedBy="mycatalog")
private List<CatalogItem> items;
// setters and getters
}
#Entity
#Table(name = "catalogitem")
public class CatalogItem {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "catalogitem_id")
private int catalogitem_id;
#NotEmpty
#Size(min = 3, max = 255)
#Column(name = "name", nullable = false)
private String name;
#NotEmpty
#Column(name = "price", nullable = false)
private Double price;
#OneToOne(mappedBy="ordercatalogitem", cascade=CascadeType.ALL)
private OrderItem morderitem;
#ManyToOne
#JoinColumn(name="catalog_id", nullable=false)
private Catalog mycatalog;
// setters and getters
}
#Entity
#Table(name = "orders")
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "order_id")
private int order_id;
#NotEmpty
#Size(min = 3, max = 255)
#Column(name = "name", nullable = false)
private String name;
#NotEmpty
#Size(min = 3, max = 1024)
#Column(name = "note", nullable = false)
private String note;
#Temporal(TemporalType.TIMESTAMP)
#DateTimeFormat(pattern = "ddmmYYYY HH:mm:ss")
#Column(name = "created", nullable = false)
private Date created;
#OneToMany(mappedBy="myorder")
private Set<OrderItem> orderItems;
#ManyToOne
#JoinColumn(name="catalog_id", nullable=false)
private Catalog mycatalogorder;
#PrePersist
protected void onCreate() {
created = new Date();
}
// setters and getters
}
#Entity
#Table(name = "orderitem")
public class OrderItem {
#Id
#Column(name="catalogitem_id", unique=true, nullable=false)
#GeneratedValue(generator="gen")
#GenericGenerator(name="gen", strategy="foreign", parameters=#Parameter(name="property", value="catalogitem"))
private int catalogitem_id;
#Column(name = "quantity")
private int quantity;
#OneToOne
#PrimaryKeyJoinColumn
private CatalogItem ordercatalogitem;
#ManyToOne
#JoinColumn(name="order_id", nullable=false)
private Order myorder;
// setters and getters
}
And I am getting the exception:
org.springframework.http.converter.HttpMessageNotWritableException:
Could not write content: failed to lazily initialize a collection of
role: com.example.helios.model.Catalog.items, could not initialize
proxy - no Session; nested exception is
com.fasterxml.jackson.databind.JsonMappingException: failed to lazily
initialize a collection of role:
com.example.helios.model.Catalog.items, could not initialize proxy -
no Session
org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:271)
org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:100)
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:222)
org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:183)
org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:80)
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:126)
My versions is:
SpringFramework 4.2.4.RELEASE
Hibernate 4.3.11.Final
Jackson 2.7.4
Jacksontype 2.7.1
This is the normal Hibernate behaviour
In one to many relations, hibernate loads the father entity (Catalog in your case) but it will load the children entities List (List items and List orders in your case) in a LAZY mode
This means you can't access to these objects because they are just proxies and not real objects
This is usefull in order to avoid to load the full DB when you execute a query
You have 2 solution:
Load children entities in EAGER mode (I strongly suggest to you to not do it because you can load the full DB.... but it is something related to your scenario
You don't serialize in your JSON the children entities by using the com.fasterxml.jackson.annotation.JsonIgnore property
Angelo
A third option which can be useful if you don't want to use EAGER mode and load up everything is to use Hibernate::initialize and only load what you need.
Session session = sessionFactory.openSession();
Catalog catalog = (Catalog) session.load(Catalog.class, catalogId);
Hibernate.initialize(shelf);
More information
I had the same problem but a fixed by:
#OneToMany
#JoinColumn(name = "assigned_ingredient", referencedColumnName = "ingredient_id")
#Fetch(FetchMode.JOIN) // Changing the fetch profile you can solve the problem
#Where(clause = "active_ind = 'Y'")
#OrderBy(clause = "meal_id ASC")
private List<Well> ingredients;
you can have more information here: https://vladmihalcea.com/the-best-way-to-handle-the-lazyinitializationexception/
It's caused by an infinite loop when parsing datas to JSON.
You can solve this by using #JsonManagedReference and #JsonBackReference annotations.
Definitions from API :
JsonManagedReference (https://fasterxml.github.io/jackson-annotations/javadoc/2.5/com/fasterxml/jackson/annotation/JsonManagedReference.html) :
Annotation used to indicate that annotated property is part of two-way
linkage between fields; and that its role is "parent" (or "forward")
link. Value type (class) of property must have a single compatible
property annotated with JsonBackReference. Linkage is handled such
that the property annotated with this annotation is handled normally
(serialized normally, no special handling for deserialization); it is
the matching back reference that requires special handling
JsonBackReference: (https://fasterxml.github.io/jackson-annotations/javadoc/2.5/com/fasterxml/jackson/annotation/JsonBackReference.html):
Annotation used to indicate that associated property is part of
two-way linkage between fields; and that its role is "child" (or
"back") link. Value type of the property must be a bean: it can not be
a Collection, Map, Array or enumeration. Linkage is handled such that
the property annotated with this annotation is not serialized; and
during deserialization, its value is set to instance that has the
"managed" (forward) link.
Example:
Owner.java:
#JsonManagedReference
#OneToMany(mappedBy = "owner", fetch = FetchType.EAGER)
Set<Car> cars;
Car.java:
#JsonBackReference
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "owner_id")
private Owner owner;
Another solution is to use #JsonIgnore which will just set null to the field.
Here is my solution for this task with Hibernate. I marked hibernate releation with #JsonIgnore and use custom field for jackson, in which I check if the field is loaded. If you need serialize collection to json then you should manualy call collection getter during hibernate transaciton.
#JsonIgnore
#OneToMany(mappedBy = "myorder")
private List<OrderItem> orderItems = new ArrayList<>();
#JsonProperty(value = "order_items", access = JsonProperty.Access.READ_ONLY)
private List<OrderItem> getOrderItemsList() {
if(Hibernate.isInitialized(this.relatedDictionary)){
return this.relatedDictionary;
} else{
return new ArrayList<>();
}
}
#JsonProperty(value = "order_items", access = JsonProperty.Access.WRITE_ONLY)
private void setOrderItemsList(List<OrderItem> orderItems) {
this.orderItems = orderItems;
}
I know this is an old post but this might still help someone facing a similar issue. To solve the problem, iterate through the list of items and set the lazy-loadable collection to null. Then set your mapper to include NON-NULL
for (Catalog c : allCatalogs) {
c.setItems(null);
}
objectMapper.setSerializationInclusion(Include.NON_NULL)
Using FetchType.LAZY , if still getting the error "Could not write content: failed to lazily initialize a collection of role" , that may be probably caused by somewhere in the logic (perhaps in a controller) , Catalog is being tried to be deserialized that contains list of catalog items which is a proxy but the transaction has already ended to get that.
So create a new model ('CatalogResource' similar to catalog but without the list of items).
Then create a catalogResource object out of the Catalog (which is returned from the query)
public class CatalogResource {
private int catalog_id;
private String name;
private List<Order> orders;
}
I think the best solution to your problem (which also is the simplest) is to set your FetchType to LAZY and simply annotate the oneToMany collection fields using #transient.
Setting FetchType to EAGER isn't a good idea most times.
Best of luck.
"You don't serialize in your JSON the children entities by using the com.fasterxml.jackson.annotation.JsonIgnore property"
Add #JsonIgnore for hibernate lazy loading properties eg. #ManyToOne. That should work
#Entity
public class Group{
#Id
#GeneratedValue
private Long id;
#ManyToOne
private Group parent;
#LazyCollection(value=LazyCollectionOption.FALSE)
#OneToMany(mappedBy = "parent")
#Cascade(value = {CascadeType.ALL})
private Set<Group> children = new HashSet<Group>();
}
How do I JSON that structure? Json goes to infinite recursion .. Im using Jackson.
I need to have parent ID in my json output also .
set a #JsonBackReference on the #ManyToOne property and a #JsonManagedReference on the #OneToMany property
#Entity
public class Group{
#Id
#GeneratedValue
private Long id;
#JsonBackReference
#ManyToOne
private Group parent;
#JsonManagedReference
#LazyCollection(value=LazyCollectionOption.FALSE)
#OneToMany(mappedBy = "parent")
#Cascade(value = {CascadeType.ALL})
private Set<Group> children = new HashSet<Group>();
}
If the Competency entity points to a Group, then you might indeed go into infinite recursion. You can put a #JsonIgnore annotation on the parent and children instance variables to ignore them from being included in the JSON, or you can use a combination of #JsonBackReference/#JsonManagedReference, as the other poster suggested.
Other thing you can do is create a pojo which contains all the properties you are interested in in your service layer, cutting the hibernate connections out of the picture. Something like that:
public class GroupDto {
private Long id;
private CompetencyDto parent;
private List<CompetencyDto> children;
}
public class CompetencyDto {
private Long id;
}
This, while seeming like overwork, would give you the power your presentation model to not be dependent on your domain model. This would give you much more flexibility when constructing your views:
http://codebetter.com/jpboodhoo/2007/09/27/screen-bound-dto-s/
http://martinfowler.com/eaaDev/PresentationModel.html