I am having inconsistent object state when using Hibernate
I have class School that have a collection of student
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "LINK_SCH_STUDENT", joinColumns = #JoinColumn(name = "SCHOOL_ID"),
inverseJoinColumns = #JoinColumn(name = "STUDENT_ID"))
#LazyCollection(LazyCollectionOption.EXTRA)
private List<Students> associatedStudents;
I have a transactional method that links a student to a school:
#Transactional
LinkStudentToSchool (schoolId, StudendID){
flush();
int result = getSession().createSQLQuery("INSERT INTO " + getSchemaName()
+ ".LINK_SCH_STUDENT(SCHOOL_ID, STUDENT_ID) " +
"Values (:recipient_group_id, :contentId)")
}
I am writing integration test for this method.
#Transactional
TestLinkStudentToSchool {
school =new School ("GeorgiaTech", ....)
schoolId = saveOrUpdate (school)
studentID = saveOrUpdate (new Student ("LazyStudent", ....))
LinkStudentToSchool (schoolId, studentID)
Hibernate.initialize (school.getAssociatedStudent);
school.getAssociatedStudent // RETURN NULL !!!
}
Why is this happening, I already called a transactional method to add a student a school (update the link table), but the state of the collection of students
in the school did not get updated !!
Is this a caching issue ? is it because I have nested #transactional?
I will really appreciate any feedback
Thanks
You start by creating a Scool instance and saving it. It becomes associated to the Hibernate session. At this time, the list of students is null, because you neglected to initialize the list to an empty list (this should be the default state of a new school).
Then you execute a SQL query, like if Hibernate didn't exist and you were using JDBC. Hibernate doesn't know what this query does. It doesn't know that the query inserts a row to the join table. And it can't magically know that this corresponds to a new association between the student and the school. It can't magically initialize the field of your school in memory and replace it with a list containing the student. Don't bypass Hibernate by using SQL queries. Use the entities:
// correct default state of a school:
private List<Students> associatedStudents = new ArrayList<>();
...
School school = new School("GeorgiaTech", ....);
session.save(school);
Student student = new Student("LazyStudent", ....);
session.save(student);
// now associate the student with the school:
school.getAssociatedStudents().add(student);
// done. Hibernate will insert the row in the association table for you.
Related
I read this article of how to build Many-To-Many using composite key.
The entities are
class Student {
// ...
#OneToMany(mappedBy = "student")
Set<CourseRating> ratings;
// ...
}
class Course {
// ...
#OneToMany(mappedBy = "course")
Set<CourseRating> ratings;
// ...
}
#Entity
class CourseRating {
#EmbeddedId
CourseRatingKey id;
#ManyToOne
#MapsId("student_id")
#JoinColumn(name = "student_id")
Student student;
#ManyToOne
#MapsId("course_id")
#JoinColumn(name = "course_id")
Course course;
int rating;
// standard constructors, getters, and setters
}
#Embeddable
class CourseRatingKey implements Serializable {
#Column(name = "student_id")
Long studentId;
#Column(name = "course_id")
Long courseId;
// standard constructors, getters, and setters
// hashcode and equals implementation
}
Now, let's assume, I want to get students that has specific courseId.
How will my JPQ look like then:
select s from Student s join fetch s.ratings r where r.id.courseId=:courserId
OR
select s from Student s join fetch s.ratings r where r.course.id=:courserId
or it would be totally different?
You should check the SQL carefully on this sort of JPA queries because there is probably more going on there than you realize.
You did a great job of setting up the ManyToMany relation with the attribute rating and your second query will work but could easily cause you problems and possible performance considerations.
For getting only the students the correct query could be
em.createQuery("select s from Student s join fetch s.ratings r where r.course.id = 1", Student.class).getResultList().forEach(s->System.out.println(s + ":" + s.getRatings()));
That gives the logs
Hibernate: select student0_.id as id1_2_0_, ratings1_.course_id as course_i1_1_1_, ratings1_.student_id as student_2_1_1_, ratings1_.rating as rating3_1_1_, ratings1_.student_id as student_2_1_0__, ratings1_.course_id as course_i1_1_0__ from Student student0_ inner join CourseRating ratings1_ on student0_.id=ratings1_.student_id where ratings1_.course_id=1
Hibernate: select course0_.id as id1_0_0_ from Course course0_ where course0_.id=?
Student(id=1):[CourseRating(id=model.CourseRatingKey#dd5, student=Student(id=1), course=Course(id=1), rating=11)]
Student(id=2):[CourseRating(id=model.CourseRatingKey#e10, student=Student(id=2), course=Course(id=1), rating=21)]
JPA will generate an extra query to get the course information since it was not prefetched. You can solve that by prefetching it yourself.
em.createQuery("select s from Student s join fetch s.ratings r join fetch r.course c where c.id = 1", Student.class).getResultList().forEach(s->System.out.println(s + ":" + s.getRatings()));
and that gives the logs
Hibernate: select student0_.id as id1_2_0_, ratings1_.course_id as course_i1_1_1_, ratings1_.student_id as student_2_1_1_, course2_.id as id1_0_2_, ratings1_.rating as rating3_1_1_, ratings1_.student_id as student_2_1_0__, ratings1_.course_id as course_i1_1_0__ from Student student0_ inner join CourseRating ratings1_ on student0_.id=ratings1_.student_id inner join Course course2_ on ratings1_.course_id=course2_.id where course2_.id=1
Student(id=1):[CourseRating(id=model.CourseRatingKey#dd5, student=Student(id=1), course=Course(id=1), rating=11)]
Student(id=2):[CourseRating(id=model.CourseRatingKey#e10, student=Student(id=2), course=Course(id=1), rating=21)]
There is an issue in that you have students with only a partial set of course ratings. If you attempt to update a rating for a student I think you will cause the other course ratings to be lost. Perhaps not a problem for your use case but you can also get the course with the list of students:
em.createQuery("select distinct c from Course c join fetch c.ratings r join fetch r.student where c.id = 1", Course.class).getSingleResult().getRatings().forEach(r->System.out.println(r.getStudent() + ":" + r));
And that will give the same results with a slightly different query and you can update a student's rating without affecting other courses.
Hibernate: select distinct course0_.id as id1_0_0_, ratings1_.course_id as course_i1_1_1_, ratings1_.student_id as student_2_1_1_, student2_.id as id1_2_2_, ratings1_.rating as rating3_1_1_, ratings1_.course_id as course_i1_1_0__, ratings1_.student_id as student_2_1_0__ from Course course0_ inner join CourseRating ratings1_ on course0_.id=ratings1_.course_id inner join Student student2_ on ratings1_.student_id=student2_.id where course0_.id=1
Student(id=2):CourseRating(id=model.CourseRatingKey#e10, student=Student(id=2), course=Course(id=1), rating=21)
Student(id=1):CourseRating(id=model.CourseRatingKey#dd5, student=Student(id=1), course=Course(id=1), rating=11)
This can also effect the JSON formatting if you are creating a REST service. In the first instance you have an multiple arrays of CourseRatings with one entry and in the second instance you have just have one array of CourseRatings with rating and student entries. Basically a partial list of Students each with a partial list of Courses is not an accurate representation of your database where as a Course with a complete list of its Students is. I'm not sure at this point which one is more efficient on the SQL server but there should be a lot less Courses than Students so if you want all the students for a Course it is probably better this way.
With JPA you should check the SQL and test your use cases carefully. Since there are lots of students and several courses per student the performance or memory overhead could be worth considering. Note also that the results can get even stranger if you were not using fetch in your queries to begin with.
This is the correct version:
SELECT s
FROM Student s
JOIN FETCH s.ratings r WHERE r.course.id = :courserId
You can put Student object inside of your embeddable id as well, in case you don't need its id. That way it will be easier to read your code, and you won't have to make #Column(name = "student_id") Long studentId to be non-insertable and non-updatable.
I have a (classic) many to many relationship using Hibernate and a Oracle database.
I defined my entities as follows.
Student.java
#Entity
#Table
public class Student implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name = "STUDENT_ID")
private Long studentId;
#ManyToMany
#JoinTable(name = "STUDENT_PROJECT", joinColumns = {
#JoinColumn(name = "STUDENT_ID") }, inverseJoinColumns = {
#JoinColumn(name = "PROJECT_ID") })
private Set<Project> projects = new HashSet<>();
Project.java
#Entity
#Table
public class Project implements Serializable {
#Id
#Column(name="PROJECT_ID")
private int projectId;
#ManyToMany(mappedBy="projects")
private Set<Student> students = new HashSet<>();
I have a STUDENT_PROJECT table in my oracle database that consists of two fields, PROJECT_ID and STUDENT_ID with a composite primary key on them.
I have a sequence on my STUDENT table to auto generate their ID.
TRIGGER STUDENT_TRIGGER
BEFORE INSERT ON STUDENT
FOR EACH ROW
BEGIN
SELECT STUDENT_SEQUENCE.NEXTVAL
INTO :STUDENT_ID
FROM dual;
END;
Now my problem is that when I try to persist my Student entity, the ID field of the STUDENT table doesn't correspond to the STUDENT_ID field of STUDENT_PROJECT table.
Somehow it persists an ID for Student and a different one in the mapping table and I can't figure out why.
This is how I manipulate my objects
Student student = new Student();
// set some fields
Set<Project> projects = new HashSet<>();
// call to a private method to set its projects
student.setProjects(projects);
studentDao.persist(student);
I had to remove the foreign key in the STUDENT_PROJECT table on the ID of STUDENT (else the constraint wouldn't let me save of course) to finally notice that is was setting differents ID's but I don't understand why.
If you need more information let me know, I tried to keep it as small as possible, thanks.
UPDATE:
I have tried to remove the trigger on the STUDENT table and changed its Java configuration to
#Id
#GeneratedValue(strategy = GenerationType.AUTO, generator = "G1")
#SequenceGenerator(name = "G1", sequenceName= "STUDENT_SEQUENCE")
#Column(name = "STUDENT_ID")
private Long studentId;
I now get:
javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [fr.persistance.data.Student#3037]
It doesn't save to database at all and produces the above exception on the persist(), I guess I have a problem with my sequence as it seems it tries to persist two Student objects with the same ID (I am looping to save multiples students and their projects).
I have used the eclipse debug to inspect a few of my Student objects after the persist() call and they each have a different studentId in Java but once the loop and the transaction end, the exception occurs but it does seem like they each get a separate ID.
The problem is that the Ids of the students are generated by the database in a trigger, while Hibernate tries to create the value itself.
For an oracle database, GenerationType.AUTO will use a sequence. The sequence is accessed by Hibernate, which assigns the value (before the student is actually inserted in the database).
To make Hibernate use your sequence, add
#SequenceGenerator(name = "sequence-generator", sequenceName = "STUDENT_SEQUENCE")
and remove the trigger.
When you want to use a trigger, you need to map it as GenerationType.IDENTITY. (Not recommended though.)
As you know, you can declare a OneToMany association with ExtraLazy semantics, in Hibernate, and that means that size(), contains() and others might become smarter. It will run specific SQL statement and will not initialize the entire collection. Great feature for large collection.
The thing is that looks like the contains() method, only compares by Id (primary key). Suppose this example:
class Department {
........
#OneToMany(mappedBy = "department", cascade = CascadeType.ALL, fetch= FetchType.LAZY)
#LazyCollection(LazyCollectionOption.EXTRA)
private Collection<Employee> employees = new ArrayList<Employee>();
.........
public void addEmployee(Employee e) {
if (!this.employees.contains(e)) {
employees.add(e);
e.setDepartment(this);
}
}
}
The code above is a Department class with a OneToMany association to Employee (another Entity), using ExtraLazy. And on the addEmployee() method, I use the method contains() of the PersistentBag in this case.
When I see the SQL statements generated by Hibernate, I see that it issued a SQL comparing by ID of the Employee.
What if I want to check if the Employee exists by name, due to I don't want to have Employee with the same name in the Deparment? or other field of the Employee? Is there a way to do that?
I need to load a collection of objects from DB via HQL with the purpose of using those objects only for setting an association in some new records.
Consider the following: I need to send an email to all students with nr of credits > 50 and I need to keep a trace of all the recipients included in the transmission.
First select the students:
Query query = sessionFactory.getCurrentSession().createQuery("from Student student left join fetch student.scores where student.credits>50");
List<Student> students = query.list();
this will return a list of students with all columns (non association attributes like first name, last name, date of birth...) loaded for each record.
The problem is that when I have a lot of data, the above query is very slow and occupies a lot of memory due to a large amount of useless data being transferred from db to app server.
I can't make all attributes lazy loaded directly in the entity as this will kill other areas of the application.
What I need is a way of loading the above collection with only the ids fetched as I am using the objects just to set them for some new objects in an association. I know this can be done for OneToMany associations easily but how can I do it for a direct queried collection?
List<Recipient> emailRecipients = new ArrayList<>();
for(Student student: students){
Recipient rec = new Recipient();
//this is the only usage of the student object
rec.setStudent(student);
...
set other properties for the recipient
sessionFactory.getCurrentSession().save(rec);
}
Inside the recipient, the Student object is setup like this:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "STD_ID", nullable = false)
private Student student;
As you can see, only the id of the Student object is needed, but I don't know how to do it.
You can use one Hibernate specific trick, which allows you to set the FK association even if you don't provide an actual managed entity. Let's say we have a studentId, we can simply set:
Student student = new Student();
student.setId(studentId);
rec.setStudent(student);
This is not supported by standard JPA, but it works with Hibernate. Just make sure you don't have any cascade propagation from child to parent (which you should not have anyway).
In the end I've loaded the studentId only and modified the mapping inside the recipient to include the studentId as well:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "STD_ID", insertable=false, updatable = false)
private Student student;
#Column(name = "STD_ID", nullable = false)
private Long studentId;
now I can simply set the studentId and the performance is much better.
I do have a problem query problem with spring and hibernate.
I've got a class called Car which maps ManyToMany to my class Inventory. Inventory btw holds no references to the Car class.
This causes spring and hibernate to create the mapping table car_loading with a fk to the car and a fk to the inventory table.
I now want to query the inventory for a special car:
String squery = "SELECT i from Inventory i, car_loading loads WHERE i.id = loads.loading AND car = ?";
But I am getting the exception
org.hibernate.hql.ast.QuerySyntaxException: car_loading is not mapped
FYI: Hibernate doesn't support the JOIN ON x.a = y.b leading me to do it that way...
Thanks inn advance for any help!
EDIT - My Mapping
public class Car {
#OneToOne
private Driver driver;
#ManyToMany(cascade=CascadeType.ALL)
private List<Inventory> loading = new ArrayList<Inventory>();
#Temporal(TemporalType.TIMESTAMP)
#DateTimeFormat(pattern="dd:MM:yyy HH:mm")
private Date lastModified;
//...
}
public class Inventory {
private Integer shouldAmount;
private Integer minAmount;
private Integer isAmount;
#Temporal(TemporalType.TIMESTAMP)
#DateTimeFormat(pattern="dd:MM:yyy HH:mm")
private Date lastModified;
//..
}
You should never use the mapping table explicitly, hibernate adds it automatically when you use entity's properties.
For your situation the query should look like:
"select c.loading from Car c where c = ?"
or just get a car object Car car = session.get(Car.class, id), then use getter as ususal Collection<Inventory> loading = car.getLoading();
I saw this question and wanted to updated it. I did it the wrong way around. I could simply query the car and return all inventorys within this car. Because there is a relation from car to inventory, but not the other way around. So query a specific car and simply return the inventory list attribute did it for me...