Do bidirectional entities require both sides to add each other in Hibernate? - java

Do bidirectional entities (OneToMany, ManyToMany) require both sides to add each other to be saved correctly in Hibernate? From my experience, they are required. Just trying to confirm my understanding.
That is, for the entitles below, are the indicated lines required?
Student student = new Student("Carl");
Course course = new Course("Science");
ReportCard reportCard = new ReportCard("A");
student.getCourses().add(course);
student.getReportCards().add(reportCard);
reportCard.setStudent(student); // <-- Is this required?
course.getStudents().add(student); // <-- Is this required?
studentRepository.save(student);
Student.java
#Entity
public class Student {
#OneToMany(cascade = CascadeType.ALL, mappedBy = "student")
private List<ReportCard> reportCards = new ArrayList<ReportCard>();
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(joinColumns = #JoinColumn(name = "student_id"), inverseJoinColumns = #JoinColumn(name = "course_id"))
private List<Course> courses = new ArrayList<Course>();
#Column
private String name;
}
ReportCard.java
#Entity
public class ReportCard {
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
private Student student;
#Column
private String grade;
}
Course.java
#Entity
public class Course {
#ManyToMany(mappedBy = "courses")
public List<Student> students = new ArrayList<>();
#Column
private String name;
}
EDIT: Replaced #JoinColumn with #Column

Hibernate (ad other JPA implementations) cares about the owning side of the association. The owning side is the side which doesn't have the mappedBy attribute. So, in your example, reportCard.setStudent(student);is required, but student.getReportCards().add(reportCard); is not.
But in general, it's indeed best to set each side of the association correctly, to have a coherent graph of objects and simply avoid bugs in your code.
Note that annotating your String fields with JoinColumn is wrong. They're not join columns, but columns. So you should annotate them with #Column, although it's also useless if you don't specify any attribute of the annotation.

In addition to the above answer, it can be helpful to manage this in the setters. In ReportCard.java
public void setStudent(Student student) {
this.student = student;
student.setReportCard(this);
}
And in Student.java:
void setReportCard(ReportCard reportCard) {
this.reportCard = reportCard;
}
So then using reportCard.setStudent(student); will set both sides for you.

Related

How to send only a list of IDs in many-to-many spring boot JPA POST request instead of sending the full object's data

I have 2 DTOs "OrderItem" and "Ingredient", both classes has #ManyToMany annotation:
#Entity
#Table
#NoArgsConstructor
#Data
public class OrderItem {
private #Id #GeneratedValue #NotNull long id;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Order order;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Food food;
private int quantity;
#ManyToMany(cascade=CascadeType.ALL)
#JoinTable(
name = "order_item_ingredient",
joinColumns = #JoinColumn(name = "order_item_id"),
inverseJoinColumns = #JoinColumn(name = "ingredient_name")
)
private Set<Ingredient> ingredients = new HashSet<>();
}
#Entity
#Table
#Data
#NoArgsConstructor
public class Ingredient {
private #Id String ingredientName;
private float basePrice;
private boolean addable;
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<Food> foods= new HashSet<>();
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<OrderItem> orderItems= new HashSet<>();
public Ingredient(String ingredientName, float basePrice, boolean addable) {
this.ingredientName = ingredientName.toLowerCase();
this.basePrice = basePrice;
this.addable = addable;
}
}
And I'm looking to add a new OrderItem using a POST request using the following #PostMapping controller function:
#PostMapping("{id}/orderItem")
public ResponseEntity<OrderItem> createMenuItem(
#PathVariable(value = "id") Long orderId,
#RequestBody OrderItem orderItem) {
Order order = orderService.getOrder(orderId)
.orElseThrow(() -> new ResourceNotFoundException("order '" + orderId + "' is not found"));
orderItem.setOrder(order);
orderItemRepository.save(orderItem);
return new ResponseEntity<>(orderItem, HttpStatus.CREATED);
}
When I send a post request to localhost:8080/1/orderItem with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1"
}
It works fine and a new order_item database record is created, but when I send the same request with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1",
"ingredients": [{"ingredientName":"leaf"}]
}
It fails and gives the following SQL error:
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'leaf' for key 'ingredient.PRIMARY'
I know that this record already exists, but how do I tell Spring Boot that I want it to look for an existing Ingredient instead of trying to create a new one?
I have an ugly solution in my mind, and that is to send the OrderItem object alongside a list of strings where each element represents a primary key for Ingredient class, then iterate through that list element by element calling the repository to get the Ingredient object then manually add it to OrderItem.ingredients, but I'm sure that is not the best solution out there.
Being defined on the OrderItem class, the relation ingredients is considered as a composition on the cascading strategy point of view. Therefore, the CascadeType.ALL implies the attempt to create the ingredient.
To avoid this, you can change the direction of this relation reverse the mappedBy information.
But then again, if you keep a CascadeType.ALL on the ingredient side, you will be in trouble if you create an ingredient with an existing orderItem. You can win on both sides an use CascadeType.ALL.
check JPA Hibernate many-to-many cascading

Why the Cascading persist does not work with many-to-many relationship in Hibernate?

Please look at the following code snippets from 2 classes (Entities) Student and Course
public class Student {
...
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "course_student",
joinColumns = #JoinColumn(name="student_id"),
inverseJoinColumns = #JoinColumn(name="course_id")
)
private List<Course> courses;
...
}
public class Course {
...
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "course_student",
joinColumns = #JoinColumn(name="course_id"),
inverseJoinColumns = #JoinColumn(name="student_id")
)
private List<Student> students;
...
}
and the driver code is as follows
try {
session.beginTransaction();
Course course = new Course("Ukulele master class");
Student student1 = new Student("Jishnu","M V","jishnumv#gmail.com");
Student student2 = new Student("Praveen","M V","praveenmv#gmail.com");
course.add(student1);
course.add(student2);
session.save(course);
session.getTransaction().commit();
}
When I run this code I am getting the following exception
Exception in thread "main" java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.jithin.entity.Student
So my doubt is. persisting the Course does not persist the associated Student(s) even though we have mentioned CascadeType.ALL in the Course class. So why the cascading is not working in the case of many to many?
nb: When I saved both the student objects using session.save(), before saving the course object. There were no exception.
You should use #JoinTable annotation only on the owning side of the #ManyToMany association.
#Entity
public class Student {
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinTable(name = "course_student",
joinColumns = #JoinColumn(name="student_id"),
inverseJoinColumns = #JoinColumn(name="course_id"))
private List<Course> courses;
// ...
}
#Entity
public class Course {
#ManyToMany(mappedBy = "courses", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<Student> students;
// ...
}
If you use bidirectional association you should take care about synchronization of both sides of association. So, you should add the following methods to your Course entity:
public class Course {
public void addStudent(Student student) {
students.add(student);
student.getCourses().add( this );
}
public void removeStudent(Student student) {
students.remove(student);
student.getCourses().remove( this );
}
// ...
}
and then you will be able to do something like this:
Course course = new Course("Ukulele master class");
Student student1 = new Student("Jishnu","M V","jishnumv#gmail.com");
Student student2 = new Student("Praveen","M V","praveenmv#gmail.com");
course.addStudent(student1);
course.addStudent(student2);
session.save(course);
As it mentioned in the documentation:
For #ManyToMany associations, the REMOVE entity state transition doesn’t make sense to be cascaded because it will propagate beyond the link table. Since the other side might be referenced by other entities on the parent-side, the automatic removal might end up in a ConstraintViolationException.
That is why you should avoid to use cascade = CascadeType.ALL in the #ManyToMany.

JPA: OneToMany relationship keeps the Collection empty

It seems I am having a difficult time understanding JPA and how the OneToMany relationship actually works.
For example, imagine I have an object Class
#Entity
public class Class {
#Id
private String className;
#OneToMany(cascade = Cascade.ALL, orphanRemoval = true)
private Set<Student> students;
// Constructors, Getters, Setter
}
I also have an object Student where it holds Class.
#Entity
public class Student {
#Id
private String studentName;
#ManyToOne
private Class class;
// Constructors, Getters, Setter
}
Obviously, a student can have multiple classes but forget about that.
Why is that when I build a class and then build a couple students using that class, findAll() on the ClassRepository returns me an empty set of students.
Class class = new Class("CS", new HashSet<>());
classRepository.save(class); // repository has no special functions
Student student1 = new Student("1", class);
Student student2 = new Student("2", class);
studentRepository.save(student1);
studentRepository.save(student2);
classRepository.findAll() // Returns me a List<Class> with only one class object that has an empty set.
I was thinking the above code should automatically see that the two students are from that one class and so when I call buildingRepository.findAll(), it will return a Class object with the students set populated properly.
Is my understanding wrong then? Or is my code wrong? And how can I change it up to fix it?
You can choose:
1. Unidirectional #OneToMany:
#Entity
public class Class {
#Id
private String className;
#OneToMany(cascade = Cascade.ALL, orphanRemoval = true)
private List<Student> students=new ArrayList<>();
// Constructors, Getters, Setter
}
#Entity
public class Student {
#Id
private String studentName;
// Constructors, Getters, Setter
}
Now, if we persist one Class:
Class class1=new Class("name1");
class1.getStudents().add(new Student("student1Name"));
// then you can make a save of class1 in db
classRepository.save(class);
2. Unidirectional #OneToMany with #JoinColumn:
To fix the aforementioned extra join table issue, we just need to add the #JoinColumn in the mix:
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "class_id")
private List<Student> students = new ArrayList<>();
3. Bidirectional #OneToMany:
The best way to map a #OneToMany association is to rely on the #ManyToOne side to propagate all entity state changes:
#Entity
public class Class {
#Id
private String className;
#OneToMany(
mappedBy = "class",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Student> students=new ArrayList<>();
// Constructors, Getters, Setter
public void addStudent(Student student) {
students.add(student);
student.setClass(this);
}
public void removeStudent(Student student) {
students.remove(student);
student.setClass(null);
}
}
#Entity
public class Student {
#Id
private String studentName;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "class_id")
private Class class;
}
And to persist:
Class c1=new Class("className1");
c1.addStudent(new Student("StudentNAme1"));
c1.addStudent(new Student("StudentNAme2"));
c1.addStudent(new Student("StudentNAme3"));
classRepository.save(c1);

Hibernate - One to Many Mapping of Set - Annotations

Newbie to Hibernate here. I'm building a simple app to play around with Hibernate and I'm getting the hang of most of the annotations but the mappings are really confusing me.
I have a Person class and I have a Note's class. A person can have many notes associated with them but a single note will only ever correspond to a specific person.
I'm trying to set it up so that the note table has a column called person_id such that I won't need an extra person_note table in the database for the associations.
How would I go about setting up the annotations such that an extra table is not created in the database and I can associate multiple notes to a single person via an extra column in the note's table?
I've tried a few options after searching on Google such as using annotations like this but with no luck:
#JoinColumn(name="id_person", nullable=false)
Person Class
#Entity
#Table(name = "person")
public class Person {
#OneToMany()
private Set<Note> notes;
public Set<Note> getNotes() {
return notes;
}
public void setNotes(Set<Note> notes) {
this.notes = notes;
}
...
}
Note Class
#Entity
#Table (name = "note")
public class Note {
#ManyToOne
private Person person;
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
...
}
Any help would be appreciated. Thank you.
Final Working Solution
For the benefit of anyone looking in the future, I now don't have a separate table for mapping note objects to people objects. The final code (with extra lines removed) now looks like this:
Person Class
#Entity
#Table(name = "person")
public class Person {
#OneToMany(fetch = FetchType.EAGER, mappedBy = "person", cascade = CascadeType.ALL)
private Set<Note> notes;
...
}
Note Class
#Entity
#Table (name = "note")
public class Note {
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "id_person", nullable = false)
private Person person;
...
}
Misc Points
I had to add cascade = CascadeType.ALL in the Person class to ensure that when I saved a Person object that all the Note objects inside were saved.
I combined the code from chsdk and Saif. Thank you to both of you.
In your mapping annotations you should map the entities with a mappedBy property in the #OneToMany annotation and specify a joinColumn under the #ManyToOne annotation using the #JoinColumn annotation, Change your code like this:
Person class:
#OneToMany(mappedBy = "person") // "person" here refers to the person property in the notes class
private Set<Note> notes;
Notes class:
#ManyToOne
#JoinColumn(name = "id_person", nullable = false)
private Person person;
Take a look at Hibernate Collection mapping for further information.
Edit Person
#OneToMany(fetch = FetchType.EAGER , mappedBy = "person")
private Set<Note> notes;
And Note
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "person_id", nullable = false)
private Person person;
I agree with you. One can implement what you're asking with only the two tables you already have.
Here is what I would do:
Database wise:
Table Person, with:
PK column: person_id
Table Note, with:
PK column: note_id
column: person_id (nullable)
column: assigned_person_id (nullable with unique index)
Purpose:
person_id - reference to the PK of the Person table. That will define the [Person : Note] relationship, where 1 person can have multiple notes.
assigned_person_id - reference to the person assigned to the note. However to guarantee your requirement that only one person can be assigned to a note - add an unique index by that column. That will guarantee that the same person_id was on used for another note record.
Hibernate wise:
Note that the code snippet is not complete! It is just pointing the important moments.
You can look at one of the many complete examples, for instance:
http://www.mkyong.com/hibernate/hibernate-one-to-many-relationship-example-annotation/
public class Person {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "person")
public setNotes(Set<Note> notes) {
this.notes=notes;
}
...
}
#Table(name = "note", catalog = "schema_name",
uniqueConstraints = #UniqueConstraint(columnNames = "assigned_person_id"))
public class Note {
private Person person;
private Person assignedPerson;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "person_id", nullable = false)
public Person getPerson() {
return person;
}
public Person getAssignedPerson() {
return assignedPerson;
}
...
}

Hibernate triggering constraint violations using orphanRemoval

I'm having trouble with a JPA/Hibernate (3.5.3) setup, where I have an entity, an "Account" class, which has a list of child entities, "Contact" instances. I'm trying to be able to add/remove instances of Contact into a List<Contact> property of Account.
Adding a new instance into the set and calling saveOrUpdate(account) persists everything lovely. If I then choose to remove the contact from the list and again call saveOrUpdate, the SQL Hibernate seems to produce involves setting the account_id column to null, which violates a database constraint.
What am I doing wrong?
The code below is clearly a simplified abstract but I think it covers the problem as I'm seeing the same results in different code, which really is about this simple.
SQL:
CREATE TABLE account ( INT account_id );
CREATE TABLE contact ( INT contact_id, INT account_id REFERENCES account (account_id) );
Java:
#Entity
class Account {
#Id
#Column
public Long id;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "account_id")
public List<Contact> contacts;
}
#Entity
class Contact {
#Id
#Column
public Long id;
#ManyToOne(optional = false)
#JoinColumn(name = "account_id", nullable = false)
public Account account;
}
Account account = new Account();
Contact contact = new Contact();
account.contacts.add(contact);
saveOrUpdate(account);
// some time later, like another servlet request....
account.contacts.remove(contact);
saveOrUpdate(account);
Result:
UPDATE contact SET account_id = null WHERE contact_id = ?
Edit #1:
It might be that this is actually a bug
http://opensource.atlassian.com/projects/hibernate/browse/HHH-5091
Edit #2:
I've got a solution that seems to work, but involves using the Hibernate API
class Account {
#SuppressWarnings("deprecation")
#OneToMany(cascade = CascadeType.ALL, mappedBy = "account")
#Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
#JoinColumn(name = "account_id", nullable = false)
private Set<Contact> contacts = new HashSet<Contact>();
}
class Contact {
#ManyToOne(optional = false)
#JoinColumn(name = "account_id", nullable = false)
private Account account;
}
Since Hibernate CascadeType.DELETE_ORPHAN is deprecated, I'm having to assume that it has been superseded by the JPA2 version, but the implementation is lacking something.
Some remarks:
Since you have a bi-directional association, you need to add a mappedBy attribute to declare the owning side of the association.
Also don't forget that you need to manage both sides of the link when working with bi-directional associations and I suggest to use defensive methods for this (shown below).
And you must implement equals and hashCode on Contact.
So, in Account, modify the mapping like this:
#Entity
public class Account {
#Id #GeneratedValue
public Long id;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true)
public List<Contact> contacts = new ArrayList<Contact>();
public void addToContacts(Contact contact) {
this.contacts.add(contact);
contact.setAccount(this);
}
public void removeFromContacts(Contact contact) {
this.contacts.remove(contact);
contact.setAccount(null);
}
// getters, setters
}
In Contact, the important part is that the #ManyToOne field should have the optional flag set to false:
#Entity
public class Contact {
#Id #GeneratedValue
public Long id;
#ManyToOne(optional = false)
public Account account;
// getters, setters, equals, hashCode
}
With these modifications, the following just works:
Account account = new Account();
Contact contact = new Contact();
account.addToContact(contact);
em.persist(account);
em.flush();
assertNotNull(account.getId());
assertNotNull(account.getContacts().get(0).getId());
assertEquals(1, account.getContacts().size());
account.removeFromContact(contact);
em.merge(account);
em.flush();
assertEquals(0, account.getContacts().size());
And the orphaned Contact gets deleted, as expected. Tested with Hibernate 3.5.3-Final.

Categories

Resources