简体   繁体   中英

JPQL: query entities with Many-to-Many relationship modeled using a composite key

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM