Spring Criteria Api Specification enum list match list values - spring-data

I have something like this
public class Car {
private List<CarCategory> categories;
...
}
so every Car can have multiple enum categories, like "FAMILY", "SPORTCAR", "PREMIUM", "AFFORDABLE" etc
I need to be able to get all cars that have all the categories in a specified/given list, for example "all cars that have FAMILY and AFFORDABLE".
All the examples that i have found using "builder.in" assumed that a Car can only have 1 category, but this is not what i want
Any help would be very appreciated, thank you

It may not be the most optimal from the query point of view, but something like this would be functionally correct:
List<String> categories = new ArrayList<>();
cagetories = getCategoriesToFilter();
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Car> cq = cb.createQuery(Car.class);
Root<Car> root = cq.from(Car.class);
for (String category : categories){
//We create an inner join to filter those cars that do not have a category
Join<Car,CarCategory> join = root.join("categories",JoinType.INNER);
join.on(cb.equals(join.get("description"),category));
}
cq.select(root);
//Due to the inner joins, at this point all the cars that have all the categories of
//"categories" will appear.
List<Car> result = entityManager.createQuery(cq).getResultList();

Related

Build up IQueryable including additional tables based on conditions

I have an issue where we create a complex IQueryable that we need to make it more efficient.
There are 2 tables that should only be included if columns from them are being filtered.
My exact situation is complex to explain so I thought I could illustrate it with an example for cars.
If I have a CarFilter class like this:
public class CarFilter
{
public string BrandName { get;set; }
public decimal SalePrice {get; set; }
}
Let's say that we have a query for car sales:
var info = from car in cars
from carSale in carSales on carSale.BrandId == car.BrandId && car.ModelId == carSale.ModelId
from brand in carBrands on car.BrandId == brand.BrandId
select car
var cars = info.ToList();
Let's say that this is a huge query that returns 100'000 rows as we are looking at cars and sales and the associated brands.
The user only wants to see the details from car, the other 2 tables are for filtering purposes.
So if the user only wants to see Ford cars, our logic above is not efficient. We are joining in the huge car sale table for no reason as well as CarBrand as the user doesn't care about anything in there.
My question is how can I only include tables in my IQueryable if they are actually needed?
So if there is a BrandName in my filter I would include CarBrand table, if not, it's not included.
Using this example, the only time I would ever want both tables is if the user specified both a BrandName and SalePrice.
The semantics are not important here, i.e the number of records returned being impacted by the joins etc, I am looking for help on the approach
I am using EF Core
Paul
It is common for complex filtering. Just join when it is needed.
var query = cars;
if (filter.SalePrice > 0)
{
query =
from car in query
join carSale in carSales on new { car.BrandId, car.ModelId } equals new { carSale.BrandId, carSale.ModelId }
where carSale.Price >= filter.SalePrice
select car;
}
if (!filter.BrandName.IsNullOrEempty())
{
query =
from car in query
join brand in carBrands on car.BrandId equals brand.BrandId
where brand.Name == filter.BrandName
select car;
}
var result = query.ToList();

JPA CriteriaBuilder find entity which has elements with certain attributes in collection

I have an entity which contains a list of elements and now I want to search over attributes of these elements. This constraint should be "and" connected. Please see these simple example:
#Entity
public class Parent {
#Column
#Enumerated(EnumType.STRING)
private City city;
#OneToMany(...)
private List<Children> childrens;
}
#Entity
public class Children {
#Column
#Enumerated(EnumType.STRING)
private School school;
#Column
private Integer yearInSchool;
}
Now I want to find Parents in a certain city, lets say "BigCity" with children in School "AwesomeSchool" which are in class/ year 6. I want to get the search result only via CriteriaBuilder.
So far I got:
final CriteriaBuilder c = getCriteriaBuilder();
final CriteriaQuery<Parent> query = c.createQuery(Parent.class);
final Root<Parent> r = query.from(Parent.class);
query.select(r)
.where(c.and(c.equal(r.get("city"), City.BigCity)),
c.equal(r.get("childrens").get("school"), School.AwesomeSchool),
c.equal(r.get("childrens").get("yearInSchool"), 6));
Unfortunately there are two problems here:
- it looks like I can't call get("school") on the list attribute
- this will return all parents with children which are either in "AwesomeSchool" or are 6 years in the school.
Can you help me please? I thought about using a join, but there the same question is: how can I define the where part of the join so that it considers that both attributes (school and yearInSchool) have to be fulfilled at the same time.
I found similar posts about querying for objects whose children fulfill one condition - but here the children has to fulfill two conditions at the same time.
Update 1
If I use a join to assert e.g. the "school" of one child, I get so far concerning the predicate:
Predicate predicate = r.join("childrens").get("school").in(School.AwesomeSchool)
How can I reuse this joined object to assert is also for the second filter condition?
You need to JOIN and then use the JOIN object you got when forming the join when forming the WHERE clauses.
Join childrenJoin = r.join("childrens");
query.where(c.and(c.equal(r.get("city"), City.BigCity)),
c.equal(childrenJoin.get("school"), School.AwesomeSchool),
c.equal(childrenJoin.get("yearInSchool"), 6));
Perhaps you mean your JPQL to be :
SELECT p FROM Parent p JOIN p.childrens c
WHERE p.city = :theCity AND c.school = :theSchool AND c.yearInSchool = 6

Troubles with JPA criteria API and multiple subqueries

I am struggling with the JPA Criteria API for formulating a query for my data structure. Ok, my entities are as follows. I have users and groups (both share a common base class OrgEntity). Logically, users can be members in multiple groups of course. Finally, I have an entity representing a task, which has a list of potential owners (that can be either single users or whole groups). The domain model is summarized below and is given, so I cannot change it.
#Entity
#Inheritance(strategy=InheritanceType.JOINED)
abstract public class OrgEntity {
#Id
public String name;
...
}
#Entity
public class User extends OrgEntity {
public String displayName;
#ManyToMany(mappedBy="members")
public List<Group> groups;
...
}
#Entity
public class Group extends OrgEntity {
#ManyToMany
public List<User> members;
...
}
#Entity
public class Task {
#Id
public String uuid;
#ManyToMany
public List<OrgEntity> potentialOwners;
...
}
The starting point for my query is a single instance of User. I want to know all the tasks where the user is a potential owner (regardless if the user is directly contained in the potentialOwners collection or member of a group that is contained in potentialOwners).
My first attempt using a named query was as follows
SELECT DISTINCT t FROM Task AS t JOIN t.potentialOwners po
WHERE (po IN (SELECT g FROM User u JOIN u.groups g WHERE u = :user)
OR po IN (SELECT u FROM User u WHERE u = :user))
It works, but I don't know if this is the most efficient way to do this. Any suggestions?
However, I have no idea how to implement this using the criteria API. Can somebody please help me with that.
Thanks
Ok, I finally figured out how to do it. If you are interested in my solution, here it is. u is the User object, basically the query parameter and em is the EntityManager instance.
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
// specifies the result value of the query
CriteriaQuery<Task> cq = criteriaBuilder.createQuery(Task.class);
// start with the navigation at the task entity
Root<Task> from = cq.from(Task.class);
// join the potential owner organizational entities
Join<Task,OrgEntity> potentialOwners = from.join("potentialOwners");
// select the tasks but remove duplicates
CriteriaQuery<Task> select = cq.select(from).distinct(true);
// definition for subquery1: fetch the user instance
Subquery<User> subquery1 = cq.subquery(User.class);
// start at the User entities
Root<User> from1 = subquery1.from(User.class);
// select the whole user
subquery1.select(from1);
// based on the specified user
subquery1.where(criteriaBuilder.equal(from1, u));
// definition for subquery2: fetch all groups for given user
Subquery<Group> subquery2 = cq.subquery(Group.class);
// we start at the User entity
Root<User> from2 = subquery2.from(User.class);
// join to Group entities via the groups collection
Join<User, Group> groups = from2.join("groups");
// select the group entities only
subquery2.select(groups).distinct(true);
// and finally restrict to all groups of the specified user
subquery2.where(criteriaBuilder.equal(from2, u));
// order in descending order based on the unique task id
select.orderBy(criteriaBuilder.desc(from.get("uuid")));
// here we restrict to those tasks that have the potential
// owners either in the result set of subquery2 or subquery1
// additionally I've tried to filter for another restriction
// in the task (based on a like statement of the uuid)
select.where(criteriaBuilder.and(
criteriaBuilder.or(
criteriaBuilder.in(potentialOwners).value(subquery2),
criteriaBuilder.in(potentialOwners).value(subquery1)),
criteriaBuilder.like(from.<String>get("uuid"), "1%")));
TypedQuery<Task> typedQuery = em.createQuery(select);
List<Task> resultList = typedQuery.getResultList();

Category tree structure with node count using JPA

I have a category tree structure, using JPA with eclipse link.
Each category in the tree includes items, and I want to select all categories along with their items count.
The code I inherited used to calculate the number of items offline, and update the category, and use the cool JPA trick for selecting the categories like so:
#Entity
#Table(name = "categories")
#NamedNativeQuery(name = "topLevel", query = "SELECT categories.* FROM categories WHERE categories.parent_id IS NULL")
public class Category {
#Id
private Long id;
private String name;
private Integer item_count;
private Long parent_id;
#JoinColumn(name = "parent_id")
private List<Categories> categories;
}
When running the native query - I get all the categories populated in the tree structure.
However, now I have a new filter that doesn't allow the user to see all the items in the category. So I can no longer calculate the items offline.
I tried to use a join so I can calculate it online, like this:
#NamedNativeQuery(name = "topLevel", query = "SELECT categories.*, count(*) as item_count FROM categories, items_to_categories WHERE categories.id = items_to_categories.category_id and some_user_specific_query and categories.parent_id IS NULL GROUP BY categories.id")
but this doesn't populate the lower levels with the right items count.
Since I am using Eclipse Link, I can't use a formula field that is not supported in this JPA implementation.
Any ideas on how to change my model in order to get it to work?
Thanks in advance!
If you want an Item count you can either query for it, such as using JPQL. This will return an Object[] including the Category and its Integer item count. If you want to use a native SQL query, then you will need to use a SqlResultSetMapping.
If you want the item count in Category, then you could define a database VIEW that makes this appear as a column and map to the view.
Another option is to map the items of the Category, then to get the item count, just access the size() of the items in Java (you could use join or batch fetching to load the items efficiently).

JPA: Selecting entities based on multiple criterions on multiple child entities

I have a problem getting the following scenario to work. A student can take tests. A student have over time taken a few tests and got a score for each test. Each student entity have a list of tests that they have completed mapped as #OneToMany.
Now I want to select all students that have completed tests on a range of grouped criterions. I want for example to search for all students that have:
Group 1: Completed "Test 1" and got a score "between 75 and 100"
and/or
Group 2: Completed "Test 2" and got a score "between 50 and 80"
This is what I have so far but it does not do what I need (cannot search by multiple parameters meaning that I have to perform the query multiple times):
SELECT s FROM Student s JOIN s.tests t WHERE t.score BETWEEN :minScore AND :maxScore AND t.testName = :testName
Is there a way to use a single NamedQuery to achieve what I want? To retrieve all Students that have completed a test that matches at least one of the parameter groups above? I've been experimenting with joins but keep running into the wall.
I made a sample code skeleton below to illustrate what I'm trying to do.
#Entity
#NamedQueries({
#NamedQuery(name="Student.findStudentByParams", query="????????") // What should this query look like to satisfy the criteria? (see below for more detail)
})
public class Student {
// .. Some other variables that are not relevant for this example
#Id
private String name;
#OneToMany(fetch=FetchType.EAGER, mappedBy = "student")
private List<Test> tests;
// Setters and getters
}
#Entity
public class Test {
private double score;
private String testName;
// .. Some other variables that are not relevant for this example
#ManyToOne(cascade=CascadeType.ALL)
private Student student;
// Setters and getters
}
public class SearchParameters {
private double minScore;
private double maxScore;
private String testName;
public SearchParameters(String minScore, String maxScore, String testName) {
this.minScore = minScore;
this.maxScore = maxScore;
this.testName = testName;
}
// Setters and getters
}
public class MainClass {
public static List<Student> getStudents(List<SearchParameters> searchParams) {
// Database initialization stuff
// What should the query look like to find all students that match any of the combined requirements in the searchParams list?
// Is it possible to do in a single query or should i make multiple ones?
// What parameters should i set? Is it possible to put in the entire array and do some sort of join?
// Retrieve all students which matches any of these search parameters:
// Have either:
// Completed "Test 1" and got a score between 75 and 100
// and/or:
// Completed "Test 2" and got a score between 50 and 80
Query namedQuery = em.createNamedQuery("Student.findStudentByParams");
namedQuery.setParameter(??);
return (List<Student>)namedQuery.getResultList();
}
public static void main() {
List<SearchParams> searchParams = new ArrayList<SearchParams();
searchParams.add(new SearchParameters(75,100, "Test 1"));
searchParams.add(new SearchParameters(50,80, "Test 2"));
// Retrieve all students which matches any of these search parameters:
// Have either:
// Completed "Test 1" and got a score between 75 and 100
// and/or:
// Completed "Test 2" and got a score between 50 and 80
ArrayList<Student> students = getStudents(searchParams);
for(Student s: students) // Print all user that match the criteria
{
System.out.println("Name: " + s.getName());
}
}
}
You need to use Criteria Builder (and eventually the canonical Metamodel).
Try something like this (code not tested):
EntityManager em; // put here your EntityManager instance
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Student> cq = cb.createQuery(Student.class);
Root<Student> student = cq.from(Student.class);
Predicate predicate = cb.disjunction();
for (SearchParams param : searchParams) {
ListJoin<Student, Test> tests = student.join(Student_.tests);
Predicate tempPredicate1 = cb.equal(tests.get(Test_.testName), param.getTestName());
Predicate tempPredicate2 = cb.ge(tests.get(Test_.score), param.getMinScore());
Predicate tempPredicate3 = cb.le(tests.get(Test_.score), param.getMaxScore());
Predicate tempPredicate = cb.and(tempPredicate1, tempPredicate2, tempPredicate3);
predicate = cb.or(predicate, tempPredicate);
}
cq.where(predicate);
TypedQuery<Student> tq = em.createQuery(cq);
return tq.getResultList();
I don't see how it would be possible without composing the query dynamically. Consider using the Criteria API to create it.
I would design the query like this:
select s from Student s where
exists (select t.id from Test t where t.student.id = s.id and ...)
or
exists (select t.id from Test t where t.student.id = s.id and ...)
or
exists (...)
As you see, there's a repeating pattern, and all these subqueries are similar an are combined into a disjunction.