Spring boot: JPA incorrectly adds where clause - java

I have three entity classes; Student, Subject and StudentSubject.
Student has one to many relation on StudentSubject, and Subject also has one to many relation on StudentSubject.
Student class
#Entity
#Getter
#Setter
public class Student {
#Id
private String email;
#OneToMany(mappedBy = "student")
#JsonManagedReference
private List<StudentSubject> subjects;
//more elements
}
Subject class
#Entity
#Getter
#Setter
public class Subject {
#Id
#GeneratedValue
private Long id;
#ManyToOne
#JsonBackReference
private Teacher teacher;
#OneToMany(mappedBy = "student")
#JsonManagedReference
private List<StudentSubject> students;
//more elements
}
StudentSubject class
#Entity
#IdClass(StudentSubjectId.class)
#Getter
#Setter
public class StudentSubject implements Serializable {
//Primary keys
#Id
#Column(name = "subject_id")
Long subjectId;
#Id
#Column(name = "student_email")
String studentEmail;
String uid;
#ManyToOne
#JsonBackReference
#JoinColumn(name = "subject_id", insertable = false, updatable = false)
private Subject subject;
#ManyToOne
#JsonBackReference
#JoinColumn(name = "student_email", insertable = false, updatable = false)
private Student student;
}
I have 3 classes, and not 2, because there are attributes specific to each student subject pair. Hence this arrangement.
When I read a subject from repository, as such
Subject subject = subjectRepository.findByNameAndTeacher(subjectName, teacher);
subject.getStudents();
all it's details are correct, except for list of students. It is always empty.(checked this by adding breakpoint)
The queries that are executed by Hibernate/JPA are,
To get subject(?)
select
subject0_.id as id1_3_,
subject0_.name as name2_3_,
subject0_.teacher_email as teacher_3_3_
from
subject subject0_
left outer join
teacher teacher1_
on subject0_.teacher_email = teacher1_.email
where
subject0_.name =?
and teacher1_.email =?
To select student list(?)
select
students0_.student_email as student_1_2_0_,
students0_.subject_id as subject_2_2_0_,
students0_.student_email as student_1_2_1_,
students0_.subject_id as subject_2_2_1_,
students0_.uid as uid3_2_1_,
subject1_.id as id1_3_2_,
subject1_.name as name2_3_2_,
subject1_.teacher_email as teacher_3_3_2_,
teacher2_.email as email1_5_3_,
teacher2_.name as name2_5_3_
from
student_subject students0_
left outer join
subject subject1_
on students0_.subject_id = subject1_.id
left outer join
teacher teacher2_
on subject1_.teacher_email = teacher2_.email
where
students0_.student_email =?
and some more.
I think the issue here is that the last where clause is incorrectly added, and common attributes in tables are not shown once. How do I fix this?

Your mapping has a typo. In Subject class, it should be #OneToMany(mappedBy = "subject") instead of mappedBy="student" hence your wrong where clause.
This is the reason it is using
where students0_.student_email =?
instead of
where students0_.subject_id =? as it thinks the way to get to students from subject is through student_email column as indicated by your mapping.

You have not specified fetch type. This should fix it.
#OneToMany(mappedBy = "student", fetch = FetchType.EAGER)
#JsonManagedReference
private List<StudentSubject> students;

Related

Spring data JPA save and updating parent entity

I have two entities called Student and Subject. They are stored in tables in the following format
student_id
name
grade
1
John
1
subject_id
name
1
English
2
Math
subject_id
student_id
mark
1
1
75
2
1
75
**Student:**
#Table(name = "student")
#Data
public class Student {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "grade")
private int grade;
//getters and setters left out for this example
}
**Subject:**
#Table(name = "subject")
#Data
public class Subject {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#Column(name = "name")
private String name;
//getters and setters left out for this example
}
**StudentRepository:**
public interface StudentRepository extends JpaRepository<Student, Long> {
}
How do I make it so that everytime I add a student using a StudentController, the subjects are automatically added to the student.
Create the third entity for the third table, create the student object and the subject object . put it in the third entity object, create the third repository and save that, all three tables will be updated together. Just make sure your relationships are correctly mentioned and you are done.
Update your Student entity to encapsulate Subject.
The idea is to explicitly define relationship between Student and Subject , and further leverag Cascade to propagate changes :
#Table(name = "student")
#Data
public class Student {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "grade")
private int grade;
#OneToOne(cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.MERGE}, orphanRemoval = true)
#JoinColumn(name = "SUBJECT_ID")
Subject subject;
}
Note : You need to make sure that you populate Subject when storing Student.
For more clarity , explore the examples presented here : https://vladmihalcea.com/a-beginners-guide-to-jpa-and-hibernate-cascade-types/

How to remove children from parent entity record in JPA?

I have Product entity and ProductRating entity, each Product can have many ProductRatings. When Product is deleted I want to have associated ratings deleted too, but nothing works so far (also orphanRemoval set to true)...
Classes:
#Getter
#Setter
#Entity
#Table(name = "PRODUCT")
public class Product extends AbstractEntity<Long> {
#Column(nullable = false)
private String name;
private String description;
#Column(nullable = false)
#Min(value = 0)
private Float cost;
#OneToMany(mappedBy = "product",
orphanRemoval = true, cascade = CascadeType.PERSIST,
fetch = FetchType.EAGER)
//#OnDelete(action = OnDeleteAction.CASCADE)
#Fetch(value = FetchMode.SELECT)
private Set<ProductRating> productRatings;
}
#Getter
#Setter
#Entity
#Table(name = "PRODUCT_RATING")
public class ProductRating extends Rating {
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "product_id")
#NotNull(message = "Rating must be in context of Product")
private Product product;
}
After Product deletion ratings stay with deleted Product's ID
AbstractEntity implementation:
#Getter
#Setter
#MappedSuperclass
public abstract class AbstractEntity<I> implements Serializable {
private static final long serialVersionUID = 1700166770839683115L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "ID", unique = true, nullable = false)
private I id;
}
In the #OneToMany relation you need to add the cascade type delete: cascade = {CascadeType.PERSIST, CascadeType.REMOVE}
Or if you don't mind having all cascade types you can just put: cascade = CascadeType.ALL
EDIT:
Also check the name of the Product primary key in the database.
It should match the defined in the #JoinColumn annotation of ProductRating
The default database field for the attribute id of the Product class would be product_id.
However you have defined the id in AbstractEntity as name = "ID" so the #JoinColumn should be something like: #JoinColumn(name = "ID")
My alternative approach to fix this problem is to:
On parent-side relation create method with #PreRemove annotation
in this method iterate over collection with #[One/Many]ToMany annotation and call delete(obj) method for corresponding repository on child
On child-side relation create method with #PreRemove annotation
In this method set parent to null

Hibernate JOIN of Unrelated Entities Out of Scope in the Generated SQL

Since Hibernate 5.1, it started to offer the feature for us to join two unrelated entities as we do in native SQL. It is a fantastic feature! However, I recently encountered an unexpected behavior of this feature. I had a query with mixed JOINs (Left Joins and Inner Joins) with both related entities and unrelated entities. In the generated SQL, all of the unrelated entities JOINs are place in the bottom of the query, which caused this exception:
com.microsoft.sqlserver.jdbc.SQLServerException: The multi-part identifier "tlterm6_.term_id" could not be bound.
I'm baffled about how that happened and why was the feature implemented in that way (They must have a good explanation, but I have not found any solutions or explanations online yet).
Does anyone have an idea of a workaround or how to fix that?
The application is running on Hibernate 5.4.6 and SQL Server database.
Sample Entity Definition:
#Entity
#Table(name = "student")
Public class Student implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", updatable = false, nullable = false)
private Integer id;
#Column
private String first_name;
#Column
private String first_name;
#OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true)
private List<College> colleges = new ArrayList<>();
// ...Other details and getters/setters omitted
}
#Entity
#Table(name = "college")
Public class College implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", updatable = false, nullable = false)
private Integer id;
#Column
private String name;
#Column
private String description;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "student_id")
private Student student;
// ...Other details and getters/setters omitted
}
Example:
FROM Student student
JOIN Class clazz ON student.id = clazz.student_id
JOIN student.colleges college
Generated SQL:
FROM dbo.student AS student
INNER JOIN dbo.college AS college ON student.id = college.student_id
INNER JOIN dbo.class AS clazz ON student.id = clazz.student_id
The expected generated SQL should be following the same order of the JOINs, however, it places the Related/Mapped entities Joins to the top and moves the Unrelated/Unmapped entities Joins to the bottom.

Eliminating extra join in JPA query by just using the FK column instead of the related entity's ID

I have an entity relationship such that:
STUDENT many-to-one STUDENT_COURSE one-to-many COURSE
Basically, there's a many-to-many relationship between students and their courses. That relationship is represented by the STUDENT_COURSE table.
Assume I have entities set up for STUDENT, STUDENT_COURSE, and COURSE such that:
#Entity
#Table(name = "STUDENT")
public course Student {
#Id
#Column(name = "ID", nullable = false)
private Long id;
#OneToMany(mappedBy = "student")
private Set<StudentCourse> studentCoursees;
// ... other fields and getters and setters
}
#Entity
#Table(name = "COURSE")
public course Course {
#Id
#Column(name = "ID", nullable = false)
private Long id;
#OneToMany(mappedBy = "course")
private Set<StudentCourse> studentCourses;
// ... other fields and getters and setters
}
#Entity
#Table(name = "STUDENT_COURSE")
public course StudentCourse {
#Id
#Column(name = "ID", nullable = false)
private Long id;
#ManyToOne
#JoinColumn(name = "STUDENT_ID", referencedColumnName = "ID")
#NotNull
private Student student;
#ManyToOne
#JoinColumn(name = "COURSE_ID", referencedColumnName = "ID")
#NotNull
private Course course;
// ... other fields and getters and setters
}
Then I have a complicated criteria query I'm creating for search purposes that wants all of the students for a particular course. I have the courseId that I want to add to the restriction. In SQL, I'd do something like this:
select *
from STUDENT, STUDENT_COURSE
where STUDENT.ID = STUDENT_COURSE.STUDENT_ID
and STUDENT_COURSE.COURSE_ID = <courseId>
Notice that I'm only joining STUDENT and STUDENT_COURSE. With criteria and the entities set up as described above, it seems like I'm forced to join STUDENT, STUDENT_COURSE, and COURSE because I don't have a courseId field on STUDENT_COURSE:
Join<Person, PersonCourse> personCourse = root.join("personCourses");
Join<PersonCourse, Course> course = personCourse.join("course");
Predicate onlySpecificCourse = builder.equal(course.get("id"), courseId);
Is this just something where I should have BOTH the #ManyToOne field from StudentCourse to Course AND the courseId field on StudentCourse? It looks like I can do this if I declare the courseId field as:
#Column(name = "USER_ID", insertable = false, updatable = false)
private String userId;
And then the joining becomes:
Join<Person, PersonCourse> personCourse = root.join("personCourses");
Predicate onlySpecificCourse = builder.equal(personCourse.get("courseId"), courseId);
If I do this, are there any gotchas that I should watch out for? In particular, it seems strange to have setters for both courseId and course on the PersonCourse entity.
Update
I am updating my answer to offer you a solution, even though I don't like it. :-)
But first, it sounds like you wish to do this in a OOP way, but you don't want to think of the persisted data as an Object Tree, in that Person, PersonCourse and Course are all part of the same object tree, yet for some special cases, you would like to forget that fact. You can only push ORM up to a certain point, after which you will have to fall back on a native SQL.
However, I will offer an ORM solution here which you may not like, so here it goes:
Add a new attribute to PersonCourse entity and map it to the COURSE_ID column in the join table. But you have to ensure that new attribute is not used in inserts and updates.
#Column(name = "COURSE_ID", insertable = false, updatable = false)
private Long courseId;
And now you can just remove the Course Root from the equation and just use the Predicate that you showed above.
Original answer
If STUDENT_CLASS table has no other columns besides the IDs for STUDENT and CLASS relations, then just use #ManyToMany between Student and Class entities, instead of #ManyToOne, and you don't need a third entity; Hibernate will take care of it for you.
If the join table does have other columns, for example GRADE or RATING columns, then use a solution like the one described here: Mapping many-to-many association table with extra column(s).

JPA mapping annotation error org.hibernate.MappingException: Foreign key must have same number of columns as the referenced primary key

I can't propper map DB tables with JPA annotation.
Tables Subject and Place is ManyToMany through JoinTable.
Subject.java
#Entity
#Table(name = "SUBJECT")
public class Subject implements Serializable {
#Id
#Column(name = "SID")
private Integer sid;
#Column(name = "NAME")
private String name;
// getters and setters
}
SubjectPlace.java
#Entity
#Table(name = "SUBJECT_PLACE")
public class SubjectPlace implements Serializable {
#Id
#Column(name = "SPID")
private Integer spid;
#ManyToOne
#JoinColumn(name = "SUB_KEY") //Subject FK
private Subject subject;
#ManyToOne
#JoinColumn(name = "PLC_KEY") //Place FK
private Place place;
// getters and setters
}
Place.java
#Entity
#Table(name = "PLACE")
public class Place implements Serializable {
#Id
#Column(name = "PID")
private Integer pid;
#Column(name = "NAME")
private String name;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
#JoinTable(name = "SUBJECT_PLACE",
joinColumns = { #JoinColumn(name = "PLC_KEY", nullable = false, updatable = false) },
inverseJoinColumns = { #JoinColumn(name = "SUB_KEY", nullable = false, updatable = false) })
private Set<Subject> subjects;
// getters and setters
}
But than I need to link Person with Subject in selected Places. I mean that each Place has its own collection of Subject. And a Person have link to Subject whitch resides in particular Place.
like This:
Subject (M) -- (M) Place through JoinTable Subject (1) -- (M) Subject_Place (M) -- (1) Place
Person (M) -- (M) Subject_Place through JoinTable Person (1) -- (M) Person_Subject_Place (M) -- (1) Subject_Place
Person.java
#Entity
#Table(name = "PERSON")
public class Person implements Serializable {
#Id
#Column(name = "PRSID")
private Integer prsid;
#Column(name = "NAME")
private String name;
// How to annotate this code?
// I experience problem in this part of code
#OneToMany
#JoinColumn(name="SPID_KEY")
private List<SubjectPlace> subjectPlaces;
// getters and setters
}
PersonSubjectPlace.java
#Entity
#Table(name = "PERSON_SUBJECT_PLACE")
public class PersonSubjectPlace implements Serializable {
#Id
#Column(name = "PSPID") // Person_Subject_Place ID
private Integer pspid;
#ManyToOne
#JoinColumn(name = "PER_KEY") //Person FK
private Person person;
// How to annotate this code?
// I experience problem in this part of code
#ManyToOne
#JoinColumn(name = "SPID_KEY") //Subject_Place FK
private SubjectPlace subjectPlace;
// getters and setters
}
And when I try so get Persons and its Subjects, I get this error:
Caused by: org.hibernate.MappingException: Foreign key (FK2C3B79384AABC975:PERSON_SUBJECT_PLACE [SPID_KEY])) must have same number of columns as the referenced primary key (SUBJECT_PLACE [PLC_KEY,SUB_KEY])
What, How shoul I map?
In your OneToMany mapping you don't need to specify the foreign key, you just need to use mappedBy property to refer your mapping object, you can learn more about it in OneToMany Mapping Documentation, and here's what you need to map Person and PersonSubjectPlace entities:
In your Person class:
#OneToMany(mappedBy="person")
private List<PersonSubjectPlace> personsubjectPlaces;
In your PersonSubjectPlace class:
#ManyToOne
#JoinColumn(name="PRSID") //Specify the primary key of Person
private Person person;
For further information about the difference between JoinColumn and mappedBy you can take a look at this answer.
EDIT:
For the mapping between SubjectPlace and PersonSubjectPlace:
In your SubjectPlace class:
#OneToMany(mappedBy="subjectPlace")
private List<PersonSubjectPlace> personsubjectPlaces;
In your PersonSubjectPlace class:
#ManyToOne
#JoinColumn(name="SPID") //Specify the primary key of SubjectPerson
private SubjectPlace subjectPlace;
Note:
The best approach to map those classes is to use #JoinTable between Person and SubjectPlace, take a look at this #JoinTable example, because PersonSubjectPlace is pratically an asociation-entity between Person and SubjectPlace.
You should remove #Joincolumn annotation and add mappedBy variable to #OneToMany annotation.
#OneToMany(mappedBy = "spid")
You should have a variable in SubjectPlace that has a Person where you should put #JoinColumn annotation

Categories