I am using Java+JPA+Hibernate+Playframework1 in a web application and have models Classroom, Student and Group (see below). Each model has exactly one instance in database, and those instances are related to each other. Ideally, deleting a classroom should also delete it's groups, and dissociate it's students, deleting a student should dissociate it's classrooms and groups, and deleting a group should dissociate it's students, but it is not working.
|Classroom|[Many]-------[Many]|Student|
[One] [Many]
| /
| /
[Many] /
|Group|[Many]--------------ยด
When I try to delete a classroom, a student or a group they are simply not deleted. No runtime exception, no SQL error, no nothing. It just fails silently. I even activated sql debug and didn't see any delete, just selects.
Update: Somehow, courses with groups not related to students are deleted correctly.
Here are mappings:
#Entity(name = "r_classroom")
public class Classroom extends Model {
#OneToMany(mappedBy = "classroom", cascade = CascadeType.ALL, orphanRemoval = true)
public List<Group> groups = new ArrayList<>();
#ManyToMany(mappedBy = "classrooms", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
public List<Student> students = new ArrayList<>();
public String name;
}
#Entity(name = "r_student")
public class Student extends Model {
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
public List<Classroom> classrooms = new ArrayList<>();
#ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST})
public List<Group> groups = new ArrayList<>();
public String name;
}
#Entity(name = "r_group")
public class Group extends Model {
#ManyToOne
public Classroom classroom;
#ManyToMany(mappedBy = "groups", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
public List<Student> students = new ArrayList<>();
public String name;
}
The Model class is part of Play Framework. I am using Play 1.4.4.
I believe something is wrong with my mappings, but what?
I posted the source code at github.
Related
Am relatively new to springboot and I have a scenario.I have users and I have jobs. A user can apply to many jobs and a job can be applied by multiple user.I have job and user entities with bidirectional relationship as follows
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "job_applications")
#JsonBackReference
private Collection<JobApplicant> applicants= new ArrayList<>();
#ManyToMany(mappedBy = "applicants",fetch = FetchType.LAZY)
#JsonManagedReference
private Collection<Job> jobs= new ArrayList();
The challenge I want to associate a user with a job programatically and this is what I was trying in my service class
public void addJobToUserJobList(int useId,int jobId) {
Job job=jobRepository.getOne(jobId);
System.out.println("Job found: "+job.getName());
JobApplicant applicant= applicantRepo.getOne(useId);
Set<Job>jobslist= new HashSet();
jobslist.add(job);
applicant.setJobs(jobslist);
applicantRepo.save(applicant);
jobRepository.save(job);
}
When I hit applicants/1/25 end-point this code in the controller the join table is not updated.
#RequestMapping(value = "/applicants/{userid}/{jobId}", method = { RequestMethod.PUT,
RequestMethod.GET })
public void applyToJob(#PathVariable int userid,#PathVariable int jobId) {
jobService.addJobToUserJobList(userid, jobId);
}
Is there a better approach to doing this ?
Turns out the issue was the owning side of my relatioship. I was saving updating my jobApplicant entity which was not the owning side hence it could not update the join table. I made the entity the owning side and it worked as below
#ManyToMany(mappedBy = "jobs",cascade = CascadeType.ALL)
#JoinTable(name = "job_applications")
#JsonBackReference
private Collection<JobApplicant> applicants= new ArrayList<>();
#ManyToMany(fetch = FetchType.LAZY)
#JsonManagedReference
private Collection<Job> jobs= new ArrayList();
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);
Let's say we have the following three domain model entities: Company, Departament, and Employee.
#Getter #Setter #NoArgsConstrutor
public class Employee {
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "department_id", nullable = false, insertable = false, updatable = false)
private Department department;
#JoinColumn(name = "department_id", nullable = false)
private int department_id;
}
#Getter #Setter #NoArgsConstrutor
public class Department {
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "company_id", nullable = false, insertable = false, updatable = false)
private Company company;
#JoinColumn(name = "company_id", nullable = false)
private int company_id;
#OneToMany(mappedBy = "department")
private List<Employee> employees;
}
#Getter #Setter #NoArgsConstrutor
private class Company {
private String name;
#OneToMany(mappedBy = "company")
private List<Department> departments;
}
For each entity, we have Repositories which extend JpaRepository, Services, and Controllers. In each Service we #Autowire the respective Repository, and in each entity Controller we call methods from the entity Service.
My issue is the following: I cannot save an entire Company, because the Departments require a Company ID, and Employees a Deparment ID. So, firstly, in my CompanyService I save and then clear the departments list, do a saveAndFlush which assigns an ID to my company. I assign the received ID to every company_id in each entity of the previously saved departments list, then attach the list back to the company and do another saveAndFlush, and I do this one more time for the employee list.
#RestController
public class CompanyController {
#Autowire
private CompanyService companyService;
#PostMapping("/companies")
public Company createCompany(#RequestBody Company newCompany) {
return companyService.createCompany(newCompany);
}
}
#Service
public class CompanyService {
#Autowire
private CompanyRepository companyRepository;
public Company createCompany(Company company) {
List<Department> departments = new ArrayList<>(company.getDepartments());
company.getDepartments().clear();
companyRepository.saveAndFlush(company);
int company_id = company.getId();
departments.forEach (department ->
department.setCompany_id(company_id);
);
//here I save a copy of the previously saved departments, because I still need the employees
company.getDepartments().addAll(departments.stream().map(department -> department.clone(department)).collect(Collectors.toList()));
company.getDepartments().forEach(department -> department.getEmployees().clear());
companyRepository.saveAndFlush(company);
//here I assign each employee it's corresponding department ID
for (int i = 0; i < company.getDepartments().size(); i++) {
Department departmentInSavedCompany = company.getDepartments().get(i);
Department departmentWhichStillHasEmployees = departments.get(i);
departmentWhichStillHasEmployees.setId(departmentInSavedCompany.getId());
departmentWhichStillHasEmployees.getEmployees().forEach(employee -> employee.setDepartment_id(departmentInSavedCompany.getId()));
}
company.getDepartments.clear();
company.getDepartments.addAll(departments);
return companyRepository.saveAndFlush(company);
}
}
#Repository
public interface CompanyRepository extends JpaRepository<Company, Integer> {
}
I currenty do not like this implementation neither do I find it good. Which is the correct approach for this situation?
When working with JPA, do not work with IDs, work with object references.
In your case, this means removing the id attributes that duplicate the references.
In order to obtain the proper entities for IDs use JpaRepository.getOne. It will return either the entity if it is already in the 1st level cache or a proxy just wrapping the id, so it won't hit the database.
This allows you to assemble your object graph and persist it in one pass starting with the entity having no references to other entities.
You might also consider configuring cascading, if you consider entities to be part of the same Aggregate, i.e. they should be loaded and persisted together.
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.
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.