I am using Spring Boot and Jackson and Hibernate to create an API. Hibernate connects to a MySQL database.
I understand the good practices but I'm stuck on a particular point.
I have an n:m relationship that contains an extra field.
Ex: Author(id, ...) -> Written(idAuthor, idBook, date) <- Book(id, ...)
I understand how to map a traditional n:m relationship, but this technique does not apply to me this time.
For this, I found a source on the internet that showed the solution: create an intermediate class in my code that contains an Author type object and a Book type object + my additional fields.
#Entity
#Table(name = "Author")
public class Author implements Serializable {
/...
#Id
#GeneratedValue
private int id;
#OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
private Set<Written> written= new HashSet<>();
/...
}
#Entity
#Table(name = "Book")
public class Book implements Serializable{
/...
#Id
#GeneratedValue
private int id;
#OneToMany(mappedBy = "book", cascade = CascadeType.ALL)
private Set<Written> written= new HashSet<>();
/...
}
public class Written implements Serializable {
#Id
#ManyToOne
#JoinColumn(name = "idAuthor")
private Author author;
#Id
#ManyToOne
#JoinColumn(name = "idBook")
private Book book;
//Extra fields ....
}
That's a bidirectional link.
With this code, I get an infinite recursivity error:
Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: java.util.ArrayList[0]->com.exampleAPI.api.model.Book["written"])]
I tried to use #JsonIgnore, #JsonManagedReference and #JsonBackReference on the Written class, also tried to use transient keyword, but nothing worked.
I can't find any source on the internet that could help me, and neither can the documentation for this particular case.
Can someone help me?
When unhandled bidirectional relationship occurs, Jackson faces infinite recursion.
I tried to use #JsonIgnore, #JsonManagedReference and #JsonBackReference on the Written class
You need to use #JsonManagedReference and #JsonBackReference annotations separately to prevent these cycles between Book and Written. A side note, transient has nothing to do with the persistence but the serialization. JPA works with the #Transient annotation.
public class Book implements Serializable {
#OneToMany(mappedBy = "book", cascade = CascadeType.ALL)
#JsonBackReference
private Set<Written> written= new HashSet<>();
...
}
public class Written implements Serializable {
#Id
#ManyToOne
#JoinColumn(name = "idBook")
#JsonManagedReference
private Book book;
...
}
Important: Don't send database entities through REST (probably what you are up to do). Better create a DAO object without bidirectional relationship and map entities into DAOs. There are several libraries able to do that: I highly recommend MapStruct, however ModelMapper is also an option. If there is a lower number of such entities, using constructors/getters/setters would be enough.
Related
This question already has answers here:
Problem with LazyInitializationException
(2 answers)
Closed 4 months ago.
I have 3 tables in the DB and 3 JPA entities respectively in Java application.
#Data
#Entity
public class Fraud {
#Id
#Column(name = "id")
private Integer id;
#Column(name = "fraud_type")
private String fraudType;
#Column(name = "fraud_value")
private String fraudValue;
#OneToMany(mappedBy = "fraud", fetch = FetchType.EAGER)
private List<FraudActionEntity> fraudActions;
}
#Data
#Entity
public class FraudActionEntity {
#Id
#Column(name = "id")
private Integer id;
#ManyToOne
#JoinColumn(name = "fraud_id")
private Fraud fraud;
#ManyToOne
#JoinColumn(name = "action_id")
private Action action;
#Column(name = "enabled")
private Boolean enabled;
}
#Data
#Entity
public class Action {
#Id
#Column(name = "id")
private Integer id;
#Column(name = "attribute_key")
private String attributeKey;
#Column(name = "attribute_value")
private String attributeValue;
}
#Repository
public interface FraudRepository extends JpaRepository<Fraud, Integer> {
public Fraud findByFraudTypeAndFraudValue(String fraudType, String fraudValue);
}
My use case
On a certain type of fraud, I want to traverse all the actions that triggers from that type of fraud and act on them.
Access code
Fraud fraud = fraudRepository.findByFraudTypeAndFraudValue("Type", "Value");
log.info(fraud.getFraudActions().get(0).getAction());
When I above code runs, everything works OK. I get the fraud and fraudActions associations as well, without getting any error.
I was under the impression that as both entities Fraud and FraudActionEntity are fetching each other eagerly, so it should give some error like cyclic fetch/infinite fetch loop, but it didn't!
Why did it work? And when exactly will give it error like cyclic fetch error OR infinite fetch loop error? And if it does give a cyclic fetch error, can we fix it using lazy fetch at #ManyToOne side as given below:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "fraud_id")
private Fraud fraud;
Update: A simple and very effective work-around towards the LazyInitializationException is to annotate your method with #Transactional annotation. This will create and maintain the transaction while the method is being executed, thereby allowing your code to make the necessary calls to the DB's lazy init objects. Learn more about it here.
The return type of your JPA repository method should be a List of the Entity object, since the result could be more than one row (that is probably why you are getting the null of the fraud variable).
Regarding the Fetch strategy, you could use Eager on that particular association or maybe other strategies. One possible solution would be to make a second query in case you need the lazy-loaded FraudAction list of objects.
Also, as a side-note avoid using lombok data annotation, and always make sure that you have a NoArgsConstructor in your Entity/DTO classes (in your case #Data adds that by accident since it includes #RequiredArgsConstructor and you do not have any final variables.
I have a user entity with an assistant column.
Every user has an assistant but there are circles as well.
For example : User A's assistant is User B and User B's assistant is
user A.
If I use #ManyToOne and #OneToMany annotations, then, there is an infinite recursion when converting objects to JSON, even #JsonManagedReference and
#JsonBackReference didn't help.
BaseEntity:
#MappedSuperclass
#Data
public class BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
#Version
private int version;
}
User:
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Data
#EqualsAndHashCode(callSuper = true)
#Table(name = "Users")
public class User extends BaseEntity {
#Column
private String username;
#Column
private String name;
#JsonManagedReference
#ManyToOne
#JoinColumn(name = "assistant_id")
private User assistant;
#JsonBackReference
#OneToMany(mappedBy = "assistant")
private Set<User> assistants;
}
Are there any opportunity in Spring to solve this?
#JsonManagedReference/#JsonBackReference won't help, because the 'forward' references can still form a cycle, even when the 'inverse' references are not being serialized.
What you want is probably for the User assigned to the assistant property to be serialized without its own assistant property (so that any cycles break). Essentially, you have the same issue as here, except in your case A and B are the same class.
Apart from the solution described in the question I've linked to, you'll also want to specify which #JsonView to use when serializing the object. See the 'Using JSON Views with Spring' section here.
Could you create Assistant entity based on the same table and join?
I know this has been asked a lot of times, and the solution is pretty obvious, but in my case it doesn't really work, and I can't figure out how to solve it.
problem: Could not write JSON: Infinite recursion (StackOverflowError)
The setup is like this:
Employee belongs to a Department. (ManyToOne)
Department has a Employee as Manager. (OneToOne)
I didn't want to have a #OneToMany List in Department so the owning side is missing.
Employee has a Department object, but this is the Department it belongs to, not the Department he manages.
Employee.java
#Entity
#Table(name = "ts_employee")
//#JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class,property="#emp_id")
public class Employee extends AbstractEntity {
#ManyToOne
#JoinColumn(name = "dept_id")
#JsonManagedReference
private Department department;
... getters and setters
}
Department.java
#Entity
#Table(name = "ts_department")
//#JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="#dept_id")
public class Department extends AbstractEntity {
#OneToOne
#JoinColumn (name = "manager_id")
#JsonBackReference
private Employee manager;
.. other fields.. getters and setters
}
AbstractEntity.java
#MappedSuperclass
public class AbstractEntity implements Serializable {
#Id
#GeneratedValue()
private Long id;
... getters and setters
}
I've tried both solutions:
#JsonBackReference + #JsonManagedReference
I get rid of the StackOverflow, but the Department.manager is not serialized (#JsonBackReference), and not sent in the response, which in bad.
#JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="#emp_id"), which doesn't seem to do anything, and StackOverflow is thrown in my face :(
How can I solve this, hopefully without modifying the model classes?
Thanks a lot :)
The #JsonBackReference wont be serialized.
If possible try to use #JsonIdentityInfo over #JsonManagedReference and #JsonBackReference. Follow this link for documentation.
You can also try using #JsonIgnore if you don't need to maintain the relationship further in the process.
I have two entities, let's say
Person.java:
#Entity
public class Person implements Serializable {
#Id
#GeneratedValue(strategy = AUTO)
private long id;
#OneToMany(mappedBy = "personData", cascade = CascadeType.PERSIST)
private List<SkillsData> skillsData;
// ...
}
SkillsData.java
#Entity
#Table(name = "SkillsData")
public class SkillsData implements Serializable {
#Id
#GeneratedValue(strategy = AUTO)
private long id;
#JoinColumn(name = "PERSONID")
#ManyToOne(cascade = REMOVE)
private Person personData;
// ...
}
When I create a person, add a list of type SkillsData to it's skillsData field and persist it everything works with no exceptions thrown, but when I browse the database directly in the SkillsData table the field PERSONID is not populated and because of that the skills added can't be referenced to the right person.
I'm trying to fix this problem for quite some time and I'll be thankful for any help.
The problem might be in the fact that you're not setting SkillsData.personData before persisting leaving it null.
You must set it cause adding SkillsData to the Person.skillsData list is not enough since you declared this side of relationship as inverse(mappedBy attribute).
Therefore it is the SkillsData.personData non-inverse side who is responsible for establishing this relationship.
I'm trying to write a hibernate adapter for an old database schema. This schema does not have a dedicated id column, but uses about three other columns to join data.
On some tables, I need to use coalesce. This is what I came up with so far:
About the definition:
A car can have elements, assigned by the car's user or by the car's group of users.
If FORIGN_ELEMENT holds a user's name, definition will be 'u'
If FORIGN_ELEMENT holds a group's name, definition will be 'g'
This also means, one table (CAR_TO_ELEMENT) is misused to map cars to elements and cargroups to elements. I defined a superclass CarElement and subclasses CarUserElement and CarGroupElement.
state is either "active" or an uninteresting string
I set definitition and state elsewhere, we do not need to worry about this.
Use DEP_NR on the join table. If it's zero, use USR_DEP_NR. I did this with COALESCE(NULLIF()) successfully in native SQL and want to achieve the same in Hibernate with Pojos.
Okay, here we go with the code:
#Entity
#Table(name="CAR")
public class Car extends TableEntry implements Serializable {
#Id
#Column(name="DEP_NR")
private int depnr;
#Id
#Column(name="USER_NAME")
#Type(type="TrimmedString")
private String username;
#ManyToOne(fetch = FetchType.EAGER, targetEntity=CarGroup.class)
#JoinColumns(value={
#JoinColumn(name="GROUP_NAME"),
#JoinColumn(name="DEP_NR"),
#JoinColumn(name="state"),
})
private CarGroup group;
#OneToMany(fetch=FetchType.EAGER, targetEntity=CarUserElement.class, mappedBy="car")
private Set<CarUserElement> elements;
}
#Entity
#Table(name="CAR_GROUP")
public class CarGroup extends TableEntry implements Serializable {
#Id
#Column(name="DEP_NR")
private int depnr;
#Id
#Column(name="GROUP_NAME")
#Type(type="TrimmedString")
private String group;
#ManyToOne(fetch = FetchType.EAGER, targetEntity=Car.class)
#JoinColumns(value={
#JoinColumn(name="GROUP_NAME"),
#JoinColumn(name="DEP_NR"),
#JoinColumn(name="state"),
})
private Set<Car> cars;
#OneToMany(fetch=FetchType.EAGER, targetEntity=CarGroupElement.class, mappedBy="car")
private Set<CarGroupElement> elements;
}
#MappedSuperclass
public class CarElement extends TableEntry {
#Id
#ManyToOne(fetch = FetchType.EAGER, targetEntity=Element.class)
#JoinColumns(value={
#JoinColumn(name="ELEMENT_NAME"),
#JoinColumn(name="state"),
})
private Element element;
}
#Entity
#Table(name="CAR_TO_ELEMENT")
public class CarUserElement extends CarElement {
#Id
#Column(name="DEFINITION")
private char definition;
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumnsOrFormulas(value = {
#JoinColumnOrFormula(formula=#JoinFormula(value="COALESCE(NULLIF(DEP_NR, 0), USR_DEP_NR)", referencedColumnName="DEP_NR")),
#JoinColumnOrFormula(column=#JoinColumn(name="FORIGN_ELEMENT", referencedColumnName="USER_NAME")),
#JoinColumnOrFormula(column=#JoinColumn(name="STATE", referencedColumnName="STATE"))
})
private Car car;
}
#Entity
#Table(name="CAR_TO_ELEMENT")
public class CarGroupElement extends CarElement {
#Id
#Column(name="DEFINITION")
private char definition;
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumnsOrFormulas(value = {
#JoinColumnOrFormula(formula=#JoinFormula(value="COALESCE(NULLIF(DEP_NR, 0), USR_DEP_NR)", referencedColumnName="DEP_NR")),
#JoinColumnOrFormula(column=#JoinColumn(name="FORIGN_ELEMENT", referencedColumnName="GROUP_NAME")),
#JoinColumnOrFormula(column=#JoinColumn(name="STATE", referencedColumnName="STATE"))
})
private Car car;
}
I tried all available versions of hibernate (from 3.5.1 [first version with #JoinColumnsOrFormulas] up to 4.x.x), but I always get this error:
Exception in thread "main" java.lang.ClassCastException: org.hibernate.mapping.Formula cannot be cast to org.hibernate.mapping.Column
at org.hibernate.cfg.annotations.TableBinder.bindFk(TableBinder.java:351)
at org.hibernate.cfg.annotations.CollectionBinder.bindCollectionSecondPass(CollectionBinder.java:1338)
at org.hibernate.cfg.annotations.CollectionBinder.bindOneToManySecondPass(CollectionBinder.java:791)
at org.hibernate.cfg.annotations.CollectionBinder.bindStarToManySecondPass(CollectionBinder.java:719)
at org.hibernate.cfg.annotations.CollectionBinder$1.secondPass(CollectionBinder.java:668)
at org.hibernate.cfg.CollectionSecondPass.doSecondPass(CollectionSecondPass.java:66)
at org.hibernate.cfg.Configuration.originalSecondPassCompile(Configuration.java:1597)
at org.hibernate.cfg.Configuration.secondPassCompile(Configuration.java:1355)
at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1737)
at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1788)
Other hibernate users seem to have the same problem: They can't get it working with any version, see this thread and other stackoverflow questions:
https://forum.hibernate.org/viewtopic.php?f=1&t=1010559
To be more complete, here's my TrimmedString Class:
https://forum.hibernate.org/viewtopic.php?p=2191674&sid=049b85950db50a8bd145f9dac49a5f6e#p2191674
Thanks in advance!
PS: It works with joining just these three colulmns with just one DEP-NR-Column (i.e. either DEP_NR OR USR_DEP_NR using just #JoinColumns). But I need this coalesce(nullif()).
I ran into a similar problem, and it seems that the issue is that you are using a #Formula inside an #Id. Hibernate wants Ids to be insertable, and Formulas are read-only.
In my case I was able to work around the problem by making the individual columns Id properties on their own, and making the joined object a separate property. I don't know if this would work in your case since you're using two different columns in your formula, but if so your code might look something like:
#Entity
#Table(name="CAR_TO_ELEMENT")
public class CarUserElement extends CarElement {
#Id
#Column(name="DEFINITION")
private char definition;
#Id
#Column(name="DEP_NR")
private Integer depNr;
#Id
#Column(name="USR_DEP_NR")
private Integer usrDepNr;
#Id
#Column(name="FORIGN_ELEMENT")
private String userName;
#Id
#Column(name="STATE")
private String state;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumnsOrFormulas(value = {
#JoinColumnOrFormula(formula=#JoinFormula(value="COALESCE(NULLIF(DEP_NR, 0), USR_DEP_NR)", referencedColumnName="DEP_NR")),
#JoinColumnOrFormula(column=#JoinColumn(name="FORIGN_ELEMENT", referencedColumnName="USER_NAME", insertable = false, updatable = false)),
#JoinColumnOrFormula(column=#JoinColumn(name="STATE", referencedColumnName="STATE", insertable = false, updatable = false))
})
private Car car;
}
Join formulas are very fragile in Hibernate for the time being; I always had a difficult time to get them work properly.
The workaround that helped me often was to create database views which exposed the proper columns (including foreign keys that don't exist in the original tables). Then I mapped the entities to the views using classing Hibernate/JPA mappings.
Sometimes there are redundant joins in the generated SQL when using such entities, but the database optimizes such queries in most cases so that the execution plan is optimal anyway.
Another approach could be using #Subselects, which are some kind of Hibernate views, but I expect them to be less performant than the classic database views.
I ran into the cast exception as well and I'm on Hibernate 5.x.
Until Hibernate dedicates time to fix the issue, I found that while this guy's approach may not be cleanest (he even eludes to that fact!), it works.
You just need to add the #Column mappings (and get/set methods) to your association table objects that are returning null and manually set the values when you populate the relation data. Simple but effective!