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);
Related
The background of this project is that there are two types of users, suscriber users and amdin users. There are also two types of sucriber users, students and professors. Admin users can register new classrooms, and suscriber users can suscribe to classrooms to see different information such as temperature etc.
The problem is that I have to map a ManyToMany bidirectional relationship between clasrooms and suscriber users and I'm getting the following error in the Classroom class:
'Many To Many' attribute value type should not be 'SuscriberUser'
and this exception:
org.hibernate.AnnotationException: Use of #OneToMany or #ManyToMany targeting an unmapped class
This is my code:
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class User implements IUser {
private String name;
// some other fields
// constructors
// getters and setters
}
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class SuscriberUser extends User implements ISuscriberUser {
#ManyToMany(mappedBy = "suscribers")
private ArrayList<Classroom> classroomSubscriptions;
// constructors
// getters and setters
}
For example one concrete class of SuscriberUser:
#Entity
#Table(name = "student")
public class Student extends SuscriberUser {
#Id
private int studentId;
// constructors
// getters and setters
}
#Entity
#Table(name = "classroom")
public class Classroom implements IClassroom {
#Id
private int internalId;
// other fields
#ManyToMany()
#JoinTable(name = "suscribers")
private ArrayList <SuscriberUser> suscribers;
// constructors
// getters and setters
}
I have also tried using #MappedSuperclass in both classes User and SuscriberUser, but it doesn't work. I guess it's because both abstract classes don't have an id.
How can I solve this?
The User class is just a collector of fields, therefore it can become a #MappedSuperClass.
#MappedSuperClass
public abstract class User implements IUser {
private String name;
// some other fields
// constructors
// getters and setters
}
If you use #Inheritance(strategy = InheritanceType.TABLE_PER_CLASS), you have a table per class, so you need to:
remove abstract
add #Entity annotation
add #Table to define a name
add #Id to the id column.
#Entity
#Table(name = "subscriber_user")
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class SuscriberUser extends User implements ISuscriberUser {
#Id
private int id;
#ManyToMany(mappedBy = "suscribers")
private List<Classroom> classroomSubscriptions;
// constructors
// getters and setters
}
More info here.
The student class does not need an id column because is in the parent class.
#Entity
#Table(name = "student")
public class Student extends SuscriberUser {
// constructors
// getters and setters
}
Pay attention that the join table is another one. This table contains the relations between students and classrooms and it can be named subscription.
#Entity
#Table(name = "classroom")
public class Classroom implements IClassroom {
#Id
private int internalId;
// other fields
#ManyToMany
#JoinTable(
name = "subscription",
joinColumns = #JoinColumn(name = "internalId"), // id class room
inverseJoinColumns = #JoinColumn(name = "id")) // id user
private List<SuscriberUser> suscribers;
// constructors
// getters and setters
}
Really confused by how one to many works in JPA, all the documents that I read, uses both one to many and many to one in their example, and I don't know if they are necessary or not, and it doesn't work when I tried it.
My question is, suppose I have two tables, and I want to populate the College object using findCollegeData() method, so that all the student in this college are in a list when I initialize the object.
Below is my approach, I am able to store all the students in the college list using storeCollegeData() method, but I am not able to retrieve the college object fully, the student list is always empty, even though the data is in the database, and it works if I try to search for student using college name directly.
public static EntityManager entityManager = something;
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
public College {
#Id
#GeneratedValue(strategy= GenerationType.AUTO)
private int cId;
private String collegeName;
private int numOfStudent;
#OneToMany(mappedBy="collegeName", cascade=CascadeType.ALL, orphanRemoval=true)
private List<Student> studentList = new ArrayList<>();
}
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
public Student {
#Id
#GeneratedValue(strategy= GenerationType.AUTO)
private int sId;
private String name;
private String collegeName;
private String city;
}
// college.getStudentList is always empty and I don't know why
public findCollegeData(String collegeName) {
College college = entityManager.find(College.class, collegeName);
}
// Student data in the studentList are inserted into student table
public storeCollegeData(College college) {
entityManager.persist(college);
}
// This method works
public findStudent(String collegeName) {
CriteriaBuilder cb = provider.get().getCriteriaBuilder();
CriteriaQuery<Student> query = cb.createQuery(Student.class);
Root<Student> student = query.from(Student.class);
query.where(
cb.and(
cb.equal(student.get("collegeName"), collegeName)
)
);
JobStatisticDB Student = provider.get().createQuery(query).getSingleResult();
}
Am i missing something??? Is join more appropriate than map here??? I dont know wat to do man
EDITED:
Got it to work by changing both of the collegeName as the primary key of table by adding #Id annotation, however though, how can I add an sId and cId to the table, so they can have duplicate college name???? Right now, I can't have duplicate college with the same name, and student that that goes to the same college!
Final Edited:
Changed database design to use foreign key see solution below
The accepted answer is incorrect: you define relationships between entities. The mappings should be as below for a bi-directional #OneToMany
College:
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
public College {
#Id
#GeneratedValue(strategy= GenerationType.AUTO)
private int cId;
private String collegeName;
private int numOfStudent;
#OneToMany(mappedBy="college", cascade=CascadeType.ALL, orphanRemoval=true)
private List<Student> studentList = new ArrayList<>();
}
Student:
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
public Student {
#Id
#GeneratedValue(strategy= GenerationType.AUTO)
private int sId;
private String name;
private String city;
//student table has a FK column college_id
#ManyToOne
#JoinColumn(name = "college_id")
private College college;
}
EntityManager find() takes the PK as an argument:
public findCollege(int collegeId) {
College college = entityManager.find(College.class, collegeId);
college.getStudents(); //will be populated
}
public findStudent(int studentId) {
Student student = entityManager.find(Student.class, studentId);
student.getCollege(); //will be populated
student.getCollege().getStudents(); //will be populated
}
If you want to find a college by name create a JPQL or Criteria query:
The field you reference in mappedBy must contain a value that equates to College's id field. Change it to collegeName instead of city, and it should work.
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 have a one to many relationship as you see here. What I want to do is I want to save school object with student objects without setting schools of student objects. Below code works but hibernate inserts null values to school_id_fk column.
public class Student {
....
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "school_id_fk")
private School school;
}
public class School{
....
#OneToMany(cascade = CascadeType.ALL, mappedBy = "school")
private Set<Student> studentSet = new HashSet<Student>(0);
}
and main method;
School school = new School();
school.setSchoolName("school name");
Student student = new Student();
student.setStudentName("studentname1");
student.setStudentSurname("studentsurname1");
//student.setSchool(school); I don't want to set this line
Student student2 = new Student();
student2.setStudentName("studentname2");
student2.setStudentSurname("studentsurname2");
//student2.setSchool(school); I don't want to set this line
SessionFactory sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
session.beginTransaction();
school.getStudentSet().add(student);
school.getStudentSet().add(student2);
session.save(school);
session.getTransaction().commit();
session.close();
sessionFactory.close();
#Funtik method is correct, however it needs third table.
But your approach is also correct. You can have this relation bidirectional.
If you want to have your school_id_fk column not null you can use
a common pattern in hibernate with (so called) convenience method:
public class Student {
....
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "school_id_fk")
private School school;
}
public class School{
....
#OneToMany(cascade = CascadeType.ALL, mappedBy = "school")
private Set<Student> studentSet;
// this is private setter (used by hibernate internally)
private void setStudentSet(Set<Student> studentSet) {
this.studentSet = studentSet;
}
// this is public method (exposed API)
public void addStudent(Student) {
if (studentSet == null) {
studentSet = new HashSet<>();
}
student.setSchool(this);
studentSet.add(student);
}
}
As you can see (according to this pattern) you should hide studentSet as only hibernate should use setStudentSet() setter.
Private setter will do that. But you can expose public API to operate on this set - addStudent() in this case.
In addStudent(student) method student object is added to set and assigned with parent school in encapsulated way.
Summary: this is common pattern when using hibernate, hide collection setter, expose convenience methods like addStudent().
In this approach you have your FK column always filled, and you can fetch School object from Student object without HQL query. There is no need for third table in this case.
This is just alternative to #Funtik solution.
MappedBy is not needed in school entity
public class School{
....
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "school_id")
private Set<Student> studentSet = new HashSet<Student>(0);
and don't use school in the Student entity
public class Student {
....
}
I have two classes as following,
Human.java
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
public class Human implements Serializable {
private long id;
private String name;
....
}
Student.java
#Entity
#DynamicUpdate
public class Student extends MyFactories {
private List<Know> KnowList;
#OneToMany(fetch = FetchType.LAZY)
public List<Know> getKnowlist() {
return knowlist;
}
public void setKnowlist(List<Know> KnowList) {
return Knowlist;
}
}
Know.java
#Entity
public class Know implements Serializable {
private long id;
private Human hu;
private Student st;
....
#ManyToOne
public Person getHu() {
return hu;
}
#ManyToOne
public Client getSt() {
return st;
}
.... setters .....
}
Code
Know kw = new Know();
kw.setSt(studentObject);
kw.setHu(humanObject);
session.save(kw);
tx.commit();
I am able to insert into Know table but hibernate does not insert any record to student_know table which it has created.
I have found this answer but it says I need to use that method if I always want to retrieve all the records. Which I do not (at times, I may just need to retrieve the student class not list of its know)
System.out.println(this.student.getKnowList().size());
When I try to access the list it runs into following exception.
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.myproject.Student.knowList, could not initialize proxy - no Session
for select case change that #OneToMany(fetch = FetchType.LAZY) to #OneToMany(fetch = FetchType.EAGER) so you can get data inside it's list.
and for the insert i need your clarification about where is your relation or getter setter of the private Factory fac;?
you should have at least something like this :
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "YOUR_FACTORY_ID_COLUMN")
private Factory fac;
public Factory getFac(){
return fac;
}
public void setFac(Factory fac){
this.fac=fac;
}
and did factory have any id?
You need to use session.Update(studentObject) as well, to insert a row into student_know table.
Please also be aware that access to a lazy association outside of the context of an open Hibernate session will result in an exception. Link