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.
Related
I have a class:
#Getter
#Setter
#Entity(name = "car")
public class CarEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id", nullable = false)
private UUID id;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "car")
private List<WheelEntity> wheels;
private String number;
}
And a query finding an entity by one of its parameters and fetching nested collection:
#Query("SELECT c FROM car c WHERE c.number=:loanContract")
Optional<CarEntity> findByNumber(#Param("number") String number);
Everything works fine in runtime, but when I try to test it, the nested collection in the entity read from database is always null. I'm using Junit5 and H2 in-memory database.
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;
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).
Let's imagine the scenario: Entity Company and Entity Address has one-to-many bidirectional relationship. So Entity Address will look like:
#Entity
#Table(name = "address")
public class AddressHbm{
#Id
#GeneratedValue(generator = "id-generator")
#Column(name="address_id")
private long id;
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE} )
#Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
#JoinColumn(name="company_id")
private Company company = null;
#Column(name="address_name")
private String name;
// other properties and methods
}
I'm going to migrate these codes to Hibernate 4.3 where CascadeType.DELETE_ORPHAN is deprecated. When I am trying to replace CascadeType.DELETE_ORPHAN with orphanRemoval = true, it seems that orphanRemoval = true doesn't even exist in #ManyToOne.
So my question is:
Does AddressHbm use #Cascade(CascadeType.DELETE_ORPHAN) in #ManayToOne incorrectly?
If #Cascade(CascadeType.DELETE_ORPHAN) is misused here, is it valid to just remove it?
Many times I'm using #Formula in my entities. But always it was a simple query or stored procedure with parameter which I can take as filed from table. Now I need to user some property from related object. But I see exception when try to get object from DB. Please see an example below
#Entity
#Table(name = "MINISTRY")
public class Ministry {
#Id
#Column(name = "ID")
private Long id;
#Column(name = "NAME")
private String name;
// unnecessary code
}
#Entity
#Table(name = "DEPARTMENT")
public class Department {
#Id
#Column(name = "ID")
private Long id;
#Column(name = "DEP_NAME")
private String departmentName;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "MINISTRY_ID")
private Ministry ministry;
// unnecessary code
}
#Entity
#Table(name = "EMPLOYEE")
public class Employee {
#Id
#Column(name = "ID")
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "DEPARTMENT_ID")
private Department department;
#Formula("test_package.calc_something(department.ministry.id)")
private BigDecimal someMetric;
// unnecessary code
}
How I should use entity prop in #Formula.
I don't want to write something like
select d.ministry.id from Department d ...
If you read the JavaDoc of Formula you will see:
The formula has to be a valid SQL fragment
So you will have to use SQL like:
#Formula("test_package.calc_something("
+ "select DEP.MINISTRY_ID from DEPARTMENT DEP where DEP.ID = DEPARTMENT_ID"
+ ")")
private BigDecimal someMetric;
The only thing that is modified by Hibernate in the fragment before writing it to SQL: It will add the table alias to your columns (as you can't predict that). I mention that, as only a rudimentary SQL parser is used for that, which will insert the alias at wrong positions for more complex fragments.
A remark about performance: The formula is executed for every Department entity that you load, even if you only want to use the attribute for sorting or filtering (just guessing from the name of the attribute) - unless you use #Basic(fetch = FetchType.LAZY) and turn bytecode instrumentation on (or emulate that with FieldHandled).