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.
Related
I have a doubt about how the modeling of my entity would be. Come on, I have a table in the database that serves to save documents from my system, this table has the columns id, fk_id (element foreign key), fk_table (entity name) and file_name (stores the name of my file) .
I did a lot of research before posting my question here, but I didn't find anything related to it, what would my entities, user, patient and doctor?
DB:
id
fk_id
fk_table
file_name
1
21
user
test1.jpg
2
32
doctor
test2.pdf
3
61
user
test10.pdf
4
100
patient
test5.jpg
Class:
public class User{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String LastName;
// What would a one-to-many relationship look like?
}
public class patient{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
// What would a one-to-many relationship look like?
}
You can use #Where. But be aware that #Where is a Hibernate annotation. It's not in the JPA standard.
For example in the User entity: (I assume that your table is mapped to an entity called Document)
#Where( clause = "fk_table = 'user'")
#JoinColumn(name = "fk_id")
#OneToMany
private List<Document> documents = new ArrayList<>( );
The following is based only on standard JPA annotations. The idea is to create an inheritance hierarchy for the documents table. The base is:
#Entity
#Table(name = "XX_DOCUMENT")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "fk_table")
public abstract class BaseDocument {
#Id
#GeneratedValue(strategy=SEQUENCE)
private Long id;
#Column(name = "file_name")
private String fileName;
}
Here we define that all entities extending this will go to the same table, with the fk_table column to discriminate. The entities extending it are defined as follows:
#Entity
#DiscriminatorValue("doctor")
public class DoctorDocument extends BaseDocument {
#ManyToOne
#JoinColumn(name = "fk_id")
private Doctor doctor;
}
#Entity
#DiscriminatorValue("patient")
public class PatientDocument extends BaseDocument {
#ManyToOne
#JoinColumn(name = "fk_id")
private Patient patient;
}
// and so on
The interesting thing is that we are reusing the column fk_id to point to the right table. From a small experiment, Hibernate seems to not have problems with it. I would suggest that you manage the DB creation another way just to be safe.
The Doctor, Patient etc need not have a common base class, e.g.:
#Entity
#Table(name = "XX_DOCTOR")
public class Doctor {
#Id
#GeneratedValue(strategy=SEQUENCE)
private Long id;
#OneToMany(mappedBy = "doctor")
private Collection<DoctorDocument> documents = new ArrayList<>();
// any doctor-specific fields
}
#Entity
#Table(name = "XX_PATIENT")
public class Patient {
#Id
#GeneratedValue(strategy=SEQUENCE)
private Long id;
#OneToMany(mappedBy = "patient")
private Collection<PatientDocument> documents = new ArrayList<>();
// any patient-specific fields
}
// and so on
You can read a (doctor, patient, ...)'s documents from the relevant collection. You can even query BaseDocument instances based on any criteria.
You can even go ahead and do more fabcy stuff with the Java code. E.g. define an interface HasDocuments:
public interface HasDocuments<D extends BaseDocument> {
Collection<D> getDocuments();
}
Doctor, Patient, ..., implements this, so they can all be treated the same way.
Here is the entity code:
#Entity
#Table(name = "t_user")
public class User{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
//omit other fields
#OneToOne(cascade = javax.persistence.CascadeType.ALL, mappedBy = "user")
private Student student;
}
#Entity
#Table(name = "t_student")
public class Student{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#OneToOne
#JoinColumn(name = "c_user_id", referencedColumnName = "id", foreignKey = #ForeignKey(ConstraintMode.NO_CONSTRAINT), columnDefinition = "INT COMMENT 'user number'")
private User user;
}
Here is the save code:
#Service
#Transactional(rollbackFor = Exception.class)
public class UserServiceImpl implements UserService {
#Autowired
private final UserRepository userRepository;
#Autowired
private final PasswordEncoder pwdEncoder;
#Override
public Optional<UserBody> add(UserParam param) {
User user = User.from(param);
if(userRepository.countByName(user.getName()) > 0){
throw new BusinessException("user already exists");
}
user.setPassword(pwdEncoder.encode(param.getPassword()));
userRepository.save(user);
user.getStudent().setUser(user); // do this for update the column `c_user_id` of student table
return Optional.of(UserBody.from(user));
}
}
As above, when I try to save a user, It will be auto-save a student, but in the t_student table, the column c_user_id is empty. so I have to write the code user.getStudent().setUser(user) to update the column c_user_id.
so why? and what should I do to let the column c_user_id be auto-filled on saving user
ps: I don't want to change the mappedBy relation to Student, I know that could work, but I think a User can be a student, also can be another character, so if I add some other character, it will modify t_user table. This causes the table to become more and more bloated
The Student gets persisted. But you specified that the Student is responsible for maintaining the relationship between Student and User, and since Student.user is null this gets persisted in the database.
Therefore Hibernate is literally doing what you told it to do.
I have a class Subject as such,
#Entity
#IdClass(SubjectId.class)
#Data
public class Subject {
#Id
private String name;
#ManyToOne
#JsonBackReference
#Id
private Teacher teacher;
#OneToMany(mappedBy = "student")
private List<StudentSubject> students;
}
Its composite Id is defined in SubjectId class as such,
public class SubjectId implements Serializable{
private String name;
private Teacher teacher;
public SubjectId(String name, Teacher teacher) {
this.name = name;
this.teacher = teacher;
}
SubjectId(){}
}
On compiling, I get the error
org.hibernate.MappingException: collection foreign key mapping has wrong number of columns: com.example.Subject.students type: component[name,teacher]
While this points the error to students element, the error occured after I removed the autogenerated id of Subject, and replaced it with combination of name and teacher.
Anyway, students element is mapped in StudentSubject class as such,
#Entity
#IdClass(StudentSubjectId.class)
public #Data
class StudentSubject {
String studentId;
#ManyToOne
#Id
private Subject subject;
#ManyToOne
#Id
private Student student;
When this error occurs, it generally means that hibernate is having problem mapping foreign keys on a right number of columns.
In above case, the subject entity in StudentSubject class was not getting mapped to two columns, that is the name and teacher_email(which is the primary key of Teacher table).
To fix such errors, add #JoinColumns as below
class StudentSubject {
#Id
#ManyToOne
#JoinColumns({
#JoinColumn(referencedColumnName = "name"),
#JoinColumn(referencedColumnName = "teacher_email") })
private Subject subject;
}
The referencedColumnName refers to the name of the column in the foreign table.
try to set OneToMany in this way:
#OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true)
private List<StudentSubject> students = new ArrayList<>();
It is definitely too late but I will answer so maybe it helps some other people. I had similar issue to yours, my error was slightly different though:
Unable to build Hibernate SessionFactory; nested exception is org.hibernate.MappingException: collection foreign key mapping has wrong number of columns: com.bla.bla.yourPackage..className.errorField type: long
In my case the error was clear enough but still took me a while to figure out. If I apply it to your case then you had a wrong 1:M mapping in this place:
#OneToMany(mappedBy = "student")
private List<StudentSubject> students;
mappedBy should be mapped by one of the fields (columns) in the StudentSubject class. Refer to this for better understanding:
https://l-lin.github.io/2018-11-14_jpa_onetomany_composite_pk/
So, if you change "mappedBy" to something like this, it should work:
//#OneToMany(mappedBy = "student.name")
#OneToMany(mappedBy = "student.teacher")
private List<StudentSubject> students;
I have a (simplified) table structure that looks something like that:
customer table:
id name
-------------------
1 customer1
alias table:
customer_id alias
-------------------------------
1 customer one
1 customer uno
When I run the following query I easily get the list of aliases per customer:
select * from customer_alias where customer_id=1;
I would like to use this query in my hibernate to populate a list of type String. I tried using #Formula as follows:
#Entity
#Table(name = "customer")
public class Customer {
#Id
#Column(name = "id")
#GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
#Column(name="name")
private String name;
#Formula("(select alias from customer_alias where customer_id = id)")
private List<String> aliases;
// Getters, setters, etc...
}
It didn't work and I got this exception:
Could not determine type for: java.util.List, at table: customer, for columns: [org.hibernate.mapping.Formula( (select alias from customer_alias where customer_id = id) )]
Is there anyway to achieve this? Doesn't have to be with #Formula of course. Any reasonable way would be great.
Here is an SQLFiddle of my example
You could use #ElementCollection for having a List of related aliases without the need to map the whole entity:
#ElementCollection
#CollectionTable(name = "customer_alias", joinColumns = #JoinColumn(name = "customer_id") )
#Column(name = "alias")
private List<String> aliases;
See also:
Difference between #OneToMany and #ElementCollection?
I think you don't want to use OneToMany annotation as the second table is just a list of strings you want to find something more elegant that would not require me to create another entity.
You can use #ElementCollection as below:
#Entity
#Table(name="college")
class College implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name="college_id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
private int collegeId;
#Column(name="name")
private String collegeName;
#ElementCollection
#CollectionTable(name="student", joinColumns=#JoinColumn(name="college_id"))
#Column(name="student_name")
private Set<String> students;
public College() {
}
public Set<String> getStudents() {
return students;
}
public void setStudents(Set<String> students) {
this.students = students;
}
public int getCollegeId() {
return collegeId;
}
public void setCollegeId(int collegeId) {
this.collegeId = collegeId;
}
public String getCollegeName() {
return collegeName;
}
public void setCollegeName(String collegeName) {
this.collegeName = collegeName;
}
#Override
public String toString() {
return "College [collegeId=" + collegeId + ", collegeName=" + collegeName + ", students=" + students + "]";
}
}
I don't think #Formula annotation supports collection it can only be applied for single-valued properties. Can't say if if there exists any tweak.
I created two entities for the tables HOBBY and STUDENT, but I have difficulties to retrieve the information I need from the Join Table.
This is the schema:
STUDENT STUDENT_HOBBY HOBBY
-------- ----------- --------
id ------------------ student_id
name hobby_id ---------- id
lastname hobby_name
--------- ---------- --------
where
Student to student_hobby = one to many
Hobby to student_hobby = one to many
(that means a student may have many hobbies and a hobby can belong to more than one student)
This is the Student entity class:
#Entity
#Table(name="STUDENT")
public class Student {
#OneToMany
#JoinTable(name="student_hobbies", joinColumns=#JoinColumn(name="student_id"),
inverseJoinColumns=#JoinColumn(name="hobby_id"))
private Collection<Hobby> hobbies;
#Id
#Column(name="ID")
#GeneratedValue
private Integer id;
#Column(name="NAME")
private String name;
#Column(name="LASTNAME")
private String lastName;
// Getters and setters here
}
This is the Hobby entity class:
#Entity
#Table(name="HOBBY")
public class Hobby {
#OneToMany
#JoinTable(name="student_hobbies", joinColumns=#JoinColumn(name="hobby_id"),
inverseJoinColumns=#JoinColumn(name="student_id"))
private Collection<Student> students;
#Id
#Column(name="ID")
#GeneratedValue
private Integer id;
#Column(name="HOBBY_NAME")
private String hobby_name;
// Getters and setters here
}
Now I would like to implement the following DAO, but I don't know exactly how to do:
public interface MyDAO {
public void addHobbyForStudent(int student_id, int hobby_id);
public void RemoveHobbyForStudent(int student_id, int hobby_id);
}
Should I create an other entity class for the Join Table? Could someone give me some indication on the way to follow?
You can create an entity class for the join table, but you don't need to and you probably shouldn't. This will just create more code to maintain. You should create an entity for the join table if there is data in the join table you need to query. EG: if there was a start_time or something in the join table and you wanted to be able to see the start_time.
You should be using the #ManyToMany annotation when you're avoiding a join table entity. The documentation I linked to gives some good examples of how to do this:
Example 1:
// In Customer class:
#ManyToMany
#JoinTable(name="CUST_PHONES")
public Set<PhoneNumber> getPhones() { return phones; }
// In PhoneNumber class:
#ManyToMany(mappedBy="phones")
public Set<Customer> getCustomers() { return customers; }
Example 2:
// In Customer class:
#ManyToMany(targetEntity=com.example.PhoneNumber.class)
public Set getPhones() { return phones; }
// In PhoneNumber class:
#ManyToMany(targetEntity=com.example.Customer.class, mappedBy="phones")
public Set getCustomers() { return customers; }
Example 3:
// In Customer class:
#ManyToMany
#JoinTable(name="CUST_PHONE",
joinColumns=
#JoinColumn(name="CUST_ID", referencedColumnName="ID"),
inverseJoinColumns=
#JoinColumn(name="PHONE_ID", referencedColumnName="ID")
)
public Set<PhoneNumber> getPhones() { return phones; }
// In PhoneNumberClass:
#ManyToMany(mappedBy="phones")
public Set<Customer> getCustomers() { return customers; }