How to query strict IN clause using JPA - spring

I have 3 tables: student, subject, student_subject_mapping.
student and subject have many to many relation & the third table contain the mapping and marks in that specific subject.
Student:
| id | name | gpa |
|:---- |:------:| -----:|
| 1 | Tom | 7.0 |
| 2 | Jerry | 8.0 |
| 3 | Popeye | 7.5 |
Subject:
| id | name |
|:---- |:----------------:|
| 1 | Physics |
| 2 | Chemistry |
| 3 | Math |
| 4 | Computer Science |
student_subject_mapping:
| student_id | subject_id | score |
|:---------- |:----------:| -----:|
| 1 | 1 | 5.0 |
| 1 | 2 | 6.0 |
| 1 | 3 | 7.5 |
| 1 | 4 | 8.0 |
| 2 | 2 | 6.0 |
| 2 | 3 | 7.0 |
| 2 | 4 | 8.0 |
| 3 | 1 | 7.0 |
| 3 | 3 | 6.0 |
| 3 | 4 | 8.5 |
I want to select students based on subjects , let's say Physics, Chemistry, Math, Computer Science, so the combination queries will be something like:
Select students with all 4 subjects and score > 5 and gpa > 6
Select students with atleast 3 of above subjects and score > 6 and gpa > 7
Select students with atleast "n" of above subjects and score > 7 and gpa > 7
Where subjects, "n", score & gpa are dynamic
Student.java
#Getter
#Setter
#EqualsAndHashCode
#NoArgsConstructor
#AllArgsConstructor
public class Student implements BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double gpa;
#OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private Set<StudentSubjectMapping> studentSubjectMapping = new HashSet<>();
}
Subject.java
#Getter
#Setter
#EqualsAndHashCode
#NoArgsConstructor
#AllArgsConstructor
public class Subject implements BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#JsonIgnore
#OneToMany(mappedBy = "subject")
private Set<StudentSubjectMapping> studentSubjectMapping = new HashSet<>();
}
#Entity
#Table
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class StudentSubjectMapping{
#EmbeddedId
StudentSubjectMappingId StudentSubjectMappingId;
#JsonIgnore
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("studentId")
private Student student;
#JsonIgnore
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("subjectId")
private Subject subject;
private Double score;
}
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode
#Embeddable
public class StudentSubjectMappingId implements Serializable {
private static final long serialVersionUID = 1L;
private Long studentId;
private Long subjectId;
}
I tried different things(earlier without embeddedId) but nothing helps:
#Query("from Student student where student.subjects in :subjects group by student.subjects HAVING count(DISTINCT student.subjects) =:count ")
#Query("SELECT new com.nsia.model.Form(f.id, f.name, f.description, f.createdAt, g, COUNT(i.id)) from Form f " +
"LEFT JOIN f.instances JOIN f.groups g WHERE f.groups IN (?1) group by f.id")
#Query("from Student student LEFT JOIN student.subjects subjects WHERE subjects.id in :subjects group by student.subjects HAVING count(DISTINCT student.subjects.size) =:count ")
#Query("from Student student where student.id not in (SELECT stud.id from Student stud where stud.subjects not in (:subjects))")
#Query("select student from Student as student " +
"where student.id not in " +
"(select students.id from Student as students " +
"join students.subjects as subjects " +
"where subjects.id not in (:subjectIds))")
#Query(value = "select students.id from Student as students " +
"left join fetch students.subjects as subjects " +
"where subjects.id not in (:subjectIds) and students.id in (1,2,3)",
countQuery = "select count (student) from Student student left join student.subjects")
List<Long> getstudentIds(#Param("subjectIds") List<Long> subjectIds);

You could use a query like this:
#Query("select student from Student as student " +
"where student.gpa >= :minGpa and :count >= " +
"(select count(*) from student.studentSubjectMapping as m " +
"where m.subject.id in (:subjectIds) " +
"and m.score >= :minScore)")
List<Long> getStudents(#Param("count") long count, #Param("minGpa") double minGpa, #Param("minScore") double minScore, #Param("subjectIds") List<Long> subjectIds);
default List<Long> getStudents(double minGpa, double minScore, List<Long> subjectIds) {
return getStudents(subjectIds.size(), minGpa, minScore, subjectIds);
}

Related

Hibernate, select where foreign key is null

I have entity as follows:
#Entity
#Table(name = "LORRY")
#NamedQueries({
#NamedQuery(name = "Lorry.findSuitableLorries",
query = "SELECT l from Lorry l " +
"WHERE l.order IS NULL")
})
public class Lorry {
#Id
#Column(name = "ID", length = 7)
#Pattern(regexp="[a-zA-Z]{2}\\d{5}")
#Size(min = 7, max = 7)
private String regNum;
#OneToOne(mappedBy = "lorry")
private Order order;
}
Now I need to select all rows where order id is null in the LORRY table, but it doesn't seem to work as I get empty list every time. How can I check if foreign key is null using JPQL?
Try either SELECT l from Lorry l WHERE l.order.id IS NULL or SELECT l from Lorry l left join l.order o WHERE o IS NULL

JPA query to work with daterange of postgresql

Actual DB table:
Column | Type | Collation | Nullable | Default
--------+-----------+-----------+----------+---------
room | text | | |
during | daterange | | |
Working DB query:
select * from room_reservation where during && daterange('[2020-10-15,2020-11-14)');
Entity Mapping:
#Column(name = "during", columnDefinition = "daterange")
private Range<Date> during;
Jpa Repository:
#Query(value = "select r.room from Room_Occupancy r where r.during && daterange('[?1, ?2)')", nativeQuery = true)
List<Long> findOccupiedRooms(#Param("d1") Date d1, #Param("d2") Date d2);
Exception:
Caused by: org.postgresql.util.PSQLException: ERROR: invalid input syntax for type date: "?1"
Position: 65
How can I write same query in JPA?
Here, '[?1, ?2)' inside string literal parameter can't be replaced. You can use || to concat as string where the parameters can be replaced.
#Query(value = "select r.room from Room_Occupancy r where r.during &&"
+ " daterange('[' || :d1 || ',' || :d2 || ')')", nativeQuery = true)
List<Long> findOccupiedRooms(#Param("d1") Date d1, #Param("d2") Date d2);
As it's a native query use ? as parameter placeholders:
#Query(value = "select r.room from Room_Occupancy r where r.during && daterange('[?, ?)')", nativeQuery = true)
List<Long> findOccupiedRooms(#Param("d1") Date d1, #Param("d2") Date d2);
Try this code:-
#Query(value = "select r.room from Room_Occupancy r where r.during >= ?1 and r.during < ?2", nativeQuery = true)
List<String> findOccupiedRooms(Date d1,Date d2); //d1 < d2
#Query(value = "select r.room from Room_Occupancy r where r.during && daterange(''||'[' || :d1 ||',' || :d2 ||')' || '')", nativeQuery = true)
List<Long> findOccupiedRooms(#Param("d1") Date d1, #Param("d2") Date d2);
This worked

JPA/Hibernate map #OneToMany for Oracle hierarchical data

Say I have parent-child info in table Organization as followed:
id name parent_id
1 A 1
2 A1 1
3 A2 1
4 A11 2
With Oracle, I can get all descendants or ancestors of an organization using "start with/connect by". For example, the following sql will get all the subtree under "A" include itself (i.e. A, A1, A2, A11)
select * from Organization start with id=1 connect by nocycle prior id=parent_id;
Or this sql will get all ancestors of A11 including itself (i.e. A11, A1, A)
with o_hier as (select o.id, o.parent_id, CONNECT_BY_ISCYCLE as lvl from Organization o start with id=4 connect by nocycle prior parent_id = id) select o.* from Organization o, o_hier where o.id = o_hier.id union all select o.* from Organization o, o_hier where o.id = o_hier.parent_id and o_hier.lvl = 1;
Now I want to map this table into OrganizationEntity like this:
#Entity
#Table(name = "Organization")
public class OrganizationEntity {
//getter/setter omitted for readability
#Id
#Column(name = "ID")
private String id;
#Column(name = "NAME")
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#???
List<OrganizationEntity> descendants = new ArrayList<>();
#ManyToOne(fetch = FetchType.LAZY)
#???
List<OrganizationEntity> ancestors= new ArrayList<>();
}
I'm aware of possible performance issue, but can we map something like this using Hibernate/JPA?
This is a tricky one. You can use standard parent and childern mappings.
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ID")
OrganizationEntity parent;
#OneToMany(fetch = FetchType.LAZY)
#mappedBy(mappedBy="parent")
List<OrganizationEntity> childern;
And then use standard tree taversals algorithms to get all the ancestors (simple while loop) or all the descendants (some DFS variant, usually preorder).
Performace wise this wound be very slow.
Other, and better idea is just do the traversals within the database with CONNECT BY and then map the result set to objects. You can do that with pure JPA calls or Hibernate specific calls.

Distinct query in Criteria NHibernate

I need the corresponding query in Criteria language to this one (to retrieve all categories from my table but to distinct them):
SELECT DISTINCT categoryName
FROM Category
WHERE CategoryID IN (
SELECT CategoryID
FROM FoodCategory
)
ORDER BY categoryName
I have table FoodCategory table
id | FoodID | CategoryID
--------|---------------|------------
| |
| |
| |
Actually CategoryID is a foreign key that is pointing to this table here. This is table for Category:
CategoryID | categoryName | otherField
---------------|------------------|------------
| |
| |
| |
And this is table for Food:
FoodID | FoodName | otherField
---------------|------------------|------------
| |
| |
| |
Something like that should do the trick :
public List<String> retrieveFoodCategoryNames() {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<String> criteriaQuery = builder.createQuery(String.class);
Root<FoodCategory> root = criteriaQuery.from(FoodCategory.class);
// by default an inner join so it will only get the categories which have their id in the FoodCategory table
Join<FoodCategory, Category> joinCategory = root.join(FoodCategory_.category);
Fetch<FoodCategory, Category> fetchCategory = root.fetch(FoodCategory_.category);
Path<String> categoryNamePath = fetchCategory.get(Category_.categoryName);
criteriaQuery.select(categoryNamePath).distinct(true);
criteriaQuery.orderBy(builder.asc(categoryNamePath));
return entityManager.createQuery(criteriaQuery).getResultList();
}
This is not the exact same SQL request because you used a subquery where I'm using a join but it seemed more suited to this particular case. The subquery syntax is a bit more complex and I will not try to write it without compiling! ^^
If something is unclear let me know :-)

How to write a left join with many to many relationship

I have the following entities:
Entity: Department
DepartmentId (int)
Name (int)
SuperiorDepartmentId (int, foreign key to department entity)
DepartmentPermissions (ICollection)
Entity: DepartmentPermission
DepartmentId (int)
UserId (int)
Permission (String)
Departments (ICollection)
Entity: User
UserId (int)
Name (string)
DepartmentPermissions (ICollection)
I need to return in my query all of the departments (including the ones where the user don't have permission) and the name of the permission when the user has any.
Department
DepartmentId | Name | SuperiorDepartmentId
1 | Sales | null
2 | Internal Sales | 1
3 | Marketing | null
DepartmentPermissions
DepartmentId | User Id | Permission
1 | 2 | r
2 | 2 | rw
1 | 3 | rw
User
UserId | Name
1 | John
2 | Mary
3 | Paul
If I ask for the data for user Mary (id=2), we shoud have as result set:
DepartmentId | Name | SuperiorDepartmentId | Permission
1 | Sales | null | r
2 | Internal Sales | 1 | rw
3 | Marketing | null | null
How can I do this?
I'm going to presume the existence of a navigation property Department.DepartmentPermissions.
var query = from d in context.Departments
select new {
Department = d,
Permissions = d.DepartmentPermissions
.Where(dp => dp.UserId == 2)
.Select(p => p.Permission)
};
var result = query.AsEnumerable()
.Select(x =>
new {
x.Department.DepartmentId,
x.Department.Name,
x.Department.SuperiorDepartmentId,
Permissions =
string.Join(", ", x.Permissions.DefaultIfEmpty("null")
}
First the raw data are collected (query), then the final results are composed in memory. The latter is done because EF won't allow string.Join in a LINQ query. Your data structure allows for more than one permission per user in one department, hence the string.Join.
If you're absolutely sure that there will always be one permission you can do
Permission = d.DepartmentPermissions.Where(dp => dp.UserId == 2)
.Select(p => p.Permission)
.FirstOrDefault()
in query, and your done.

Resources