Dev Ramble

Guides, how-to's and other nonsensical ramblings from a software engineer

Home

Using Criteria Queries to Filter Entities With Many-To-Many Relationships

May 23, 2020

JPA Criteria Queries allow us to create complex queries in a object-oriented manner. In this post, I’ll provide an example where we create a query that filters an entity that has a many-to-many relationship with another entity.

The Example

In this example, we’ll be using Hibernate as our JPA implementation. We will have two entities, Student and Course. To keep things simple, both the entities will just have a name and id field. Additionally, Students and Courses will share a many-to-many relationship. A Student can be taking multiple Courses and a Course can have multiple Students.

Below is our implementation of the Student and Course entities.

@Entity
public class Student {

	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	private Long id;
	
	@Column(name="student_name")
	private String studentName;
	
	@ManyToMany
	@JoinTable(name = "student_course", joinColumns = @JoinColumn(name = "student_id"), 
        inverseJoinColumns = @JoinColumn(name = "course_id"))
	private Set<Course> courses;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getStudentName() {
		return studentName;
	}

	public void setStudentName(String studentName) {
		this.studentName = studentName;
	}
	
	public Set<Course> getCourses() {
		return courses;
	}

	public void setCourses(Set<Course> courses) {
		this.courses = courses;
	}

	@Override
	public String toString() {
		return "Student [id=" + id + ", studentName=" + studentName + "]";
	}
	
}
Student.java
@Entity
public class Course {
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	private Long id;
	
	@Column(name="course_name")
	private String courseName;
	
	@JsonIgnore
	@ManyToMany(mappedBy = "courses")
	private Set<Student> students;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getCourseName() {
		return courseName;
	}

	public void setCourseName(String courseName) {
		this.courseName = courseName;
	}
	
	public Set<Student> getStudents() {
		return students;
	}

	public void setStudents(Set<Student> students) {
		this.students = students;
	}

	@Override
	public String toString() {
		return "Course [id=" + id + ", courseName=" + courseName + "]";
	}
}
Course.java

The resulting database will look like the following. Students and Courses are linked by a Join Table that maps the a Student ID with a Course ID.

ERD

Database Structure

The Criteria Query

In this example, we’ll be creating a findByCourseNames method that returns students that are taking all the courses in the coursesNames argument list. To create our query, the first thing we need to do is initialize the CriteriaBuilder and CriteriaQuery. As we’re querying for students, the query root will be using the Student entity class.

The predicates list will contain all the criteria that will be used to create our query. We will be adding criteria to this list to ensure our query only returns Student results taking the required Courses.

To do this, we will need to iterate through the courseNames list. For each course name in the list, a subquery will be created that selects all student IDs for students that are taking that course.

The next step is to add criteria to our predicates list. For each subquery, we’ll add a predicate that checks that the student is one of the students selected by the subquery. The final result after adding predicates for each course is that we ensure we only find students that are present in each subquery, hence fulfilling the requirement of taking every course in the argument list.

Below is the implementation of the findByCourseNames method.

public List<Student> findByCourseNames(List<String> courseNames) {
    // Initialize the query
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Student> cq = cb.createQuery(Student.class);
    Root<Student> student = cq.from(Student.class);
    List<Predicate> predicates = new ArrayList<>();

    // Iterate over each courseName we want to match with
    for(String courseName : courseNames) {
        // Initialize the subquery
        Subquery<Long> subquery = cq.subquery(Long.class);
        Root<Student> subqueryStudent = subquery.from(Student.class);
        Join<Course, Student> subqueryCourse = subqueryStudent.join("courses");
        
        // Select the Student ID where one of their courses matches
        subquery.select(subqueryStudent.get("id")).where(
            cb.equal(subqueryCourse.get("courseName"), courseName));

        // Filter by Students that match one of the Students found in the subquery
        predicates.add(cb.in(student.get("id")).value(subquery));
    }
    
    // Use all predicates above to query
    cq.where(cb.and(predicates.toArray(new Predicate[predicates.size()])));
    TypedQuery<Student> query = entityManager.createQuery(cq);
    
    return query.getResultList();
}
findByCourseNames

Below is the SQL generated by the Criteria Query when calling the findByCourseNames method with two courseNames. As expected, this will create two subqueries, one for each course. The main query will then find students that match the IDs found by the subqueries.

select
	student0_.id as id1_1_,
	student0_.student_name as student_2_1_ 
from
	student student0_ 
where
	(
		student0_.id in (
			select
				student1_.id 
			from
				student student1_ 
			inner join
				student_course courses2_ 
					on student1_.id=courses2_.student_id 
			inner join
				course course3_ 
					on courses2_.course_id=course3_.id 
			where
				course3_.course_name=?
		)
	) 
	and (
		student0_.id in (
			select
				student4_.id 
			from
				student student4_ 
			inner join
				student_course courses5_ 
					on student4_.id=courses5_.student_id 
			inner join
				course course6_ 
					on courses5_.course_id=course6_.id 
			where
				course6_.course_name=?
		)
	)
SQL generated by Criteria Query

Example with Data

Let’s populate our tables with some data and verify that the query works as expected.

student

id student_name
1 Bob
10 Steve
11 Amy

course

id course_name
2 Math
3 Chemistry
4 Physics
5 Biology
6 English
7 French
8 Computer Science
9 History

student_course

student_id course_id
1 2
1 3
1 4
1 5
10 2
10 9
11 4
11 5
11 8

And the output of calling findByCourseNames with different inputs:

Input: ['Math']

Output:

[
    {
        "id": 10,
        "studentName": "Steve",
        "courses": [
            {
                "id": 2,
                "courseName": "Math"
            },
            {
                "id": 9,
                "courseName": "History"
            }
        ]
    },
    {
        "id": 1,
        "studentName": "Bob",
        "courses": [
            {
                "id": 2,
                "courseName": "Math"
            },
            {
                "id": 5,
                "courseName": "Biology"
            },
            {
                "id": 4,
                "courseName": "Physics"
            },
            {
                "id": 3,
                "courseName": "Chemistry"
            }
        ]
    }
]

Input: ['Biology', 'Physics']

Output:

[
    {
        "id": 1,
        "studentName": "Bob",
        "courses": [
            {
                "id": 2,
                "courseName": "Math"
            },
            {
                "id": 5,
                "courseName": "Biology"
            },
            {
                "id": 4,
                "courseName": "Physics"
            },
            {
                "id": 3,
                "courseName": "Chemistry"
            }
        ]
    },
    {
        "id": 11,
        "studentName": "Amy",
        "courses": [
            {
                "id": 5,
                "courseName": "Biology"
            },
            {
                "id": 4,
                "courseName": "Physics"
            },
            {
                "id": 8,
                "courseName": "Computer Science"
            }
        ]
    }
]

Input: ['Computer Science', 'Chemistry']

Output:

// no results
[]

Share This Post