Building a dynamic select query using Spring Data Specification - java

I'm trying to build a select query with Spring Data Specification. The query in question is the following:
SELECT * FROM product WHERE id IN (SELECT product_id FROM product_tags WHERE tags IN ('GRADUATION', 'BIRTHDAY'));
The user is supposed to provide a set of tags to be matched with the IN operator in the subquery, BIRTHDAY and GRADUATION are some examples. I've tried building my solution off this answer but ran into some trouble.
public static Specification<Product> withTags(Set<Tags> tags) {
return tags == null ?
null :
(root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
Subquery<Tags> subquery = query.subquery(Tags.class);
Root<Tags> subqueryRoot = subquery.from(Tags.class);
subquery.select(subqueryRoot.get("product_tags")
.get("product_id"));
subquery.where(criteriaBuilder.trim(subqueryRoot.get("product")
.get("id")).in(tags));
predicates.add(subqueryRoot.get("*").in(subquery));
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
The problem here is that I'm trying to create a subquery from Tags which is not registered as an entity but it is rather an enum. Thus, executing the code gives me an error (This is the only error I've encountered so far, please point out parts of the code that may potentially cause other errors).
public enum Tags {
BIRTHDAY("birthday"),
GRADUATION("graduation"),
GET_WELL_SOON("get well soon"),
RIBBON("ribbon"),
WRAPPING_PAPER("wrapping paper");
final String tagName;
private Tags(String tagName) {
this.tagName = tagName;
}
public String getTagName() {
return tagName;
}
}
Not sure if this will help, but in the Product class there is a field tags denoted with #ElementCollection. Spring automatically creates a table named 'product_tags' with this, and the subquery selects from this table.
#ElementCollection(fetch = FetchType.EAGER)
#Enumerated(EnumType.STRING)
private Set<Tags> tags;
If possible, I would like to translate this query instead of the first one
SELECT * FROM product WHERE id IN (SELECT product_id FROM product_tags WHERE tags = ANY(ARRAY['GRADUATION', 'GET_WELL_SOON']));
UPDATE
I have edited my code
public static Specification<Product> withTags(Set<Tags> tags) {
return tags == null ?
null :
(root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
Subquery<Long> subquery = query.subquery(Long.class);
Root<Product> subroot = subquery.from(Product.class);
subquery.select(subroot.get("id").get("tags"));
subquery.where(criteriaBuilder.trim(subroot.join("tags")
.get("id")).in(tags));
predicates.add(root.get("id").in(subquery));
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
But now I'm getting this error
java.lang.IllegalStateException: Illegal attempt to dereference path source [null.id] of basic type
For reference, my tables are defined as such
product:
Column | Type | Collation | Nullable | Default
-------------+------------------------+-----------+----------+---------
id | bigint | | not null |
category | character varying(255) | | |
date_added | date | | |
description | character varying(255) | | |
name | character varying(255) | | |
price | double precision | | not null |
product_tags:
Column | Type | Collation | Nullable | Default
------------+------------------------+-----------+----------+---------
product_id | bigint | | not null |
tags | character varying(255) | | |

public static Specification<Product> withTags(Set<Tags> tags) {
return tags.isEmpty() ?
null:
(root, query, criteriaBuilder) -> {
Subquery<Tags> subquery = query.subquery(Tags.class);
Root<Product> subroot = subquery.from(Product.class);
subquery.select(subroot.get("id")).where(subroot.join("tags").in(tags));
Predicate predicate = root.get("id").in(subquery);
return criteriaBuilder.and(predicate);
};
I seemed to have found an answer. Tags.class works apparently, and from there I just had to tweak my query to be a join select. Not what I initially hoped to accomplish, but it works.

Related

JPA count(*) using CriteriaBuilder. How to use CriteriaBuilder to retrive the count(*) column

I have a question about CriteriaBuilder API:
I would like to count the results of a column with returning back the result of that counting and the list of the distinct values of that column.
| Table_fruit | count(Table_fruit) |
|------------------------|---------------------------|
| apple | (5) |
| orange | (20) |
| banana | (400) |
So I want to make a query that will do this SQL statement:
select distinct COLUMN_NAME_1, count(COLUMN_NAME_1)
from TABLE_NAME
where COLUMN_NAME_2= 'value'
group by COLUMN_NAME_1;
CriteriaBuilder cb = getCriteriaBuilder();
CriteriaQuery<Fruit> cq = cb.createQuery(Fruit.class);
Root<TABLE_NAME> root = cq.from(Fruit.class);
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(root.get("COLUMN_NAME_2"), value));
cq.where(predicates.toArray(new Predicate[predicates.size()]));
// how to select the COUNT(*) from the table here?
cq.select(root.get("COLUMN_NAME_1")).distinct(true);
cq.groupBy(root.get("COLUMN_NAME_1"));
So my question is: how to retrieve the two values from the query in Java
try this
cq.multiselect(root.get("COLUMN_NAME_1"), cb.count(root.get("COLUMN_NAME_1"));
// add your predicates here
cq.groupBy(root.get("COLUMN_NAME_1"));
1) Change the generic to: CriteriaQuery<Object[]> cq = cb.createQuery(Object[]);
2) Change the select: cq.select(cb.count(root), root.get("COLUMN_NAME_1"))
3) Iterate the result list:
List<Object[]> results = em.createQuery(cq).getResultList();
for (Object[] result : results) {
// actions
}

JPA (Hibernate) / QueryDSL left join with condition doesn't work

I have two tables - user and booking. Each user may have many bookings (one-to-many relationship).
user: booking:
id | name | id | country | user_id | price |
-------------| ------------------------------------|
1 | Alice | 1 | Italy | 1 | 2000 |
2 | Bob | 2 | France | 1 | 2500 |
3 | Spain | 1 | 3000 |
I want to select all users and all bookings where booking's price is greater than 2000 using Query DSL. If a user doesn't have any bookings or bookings don't match the condition I still want to select this user.
First, let's have a look at how it would look like using a simple SQL left join query:
SELECT u.*, b.* FROM user u LEFT JOIN booking b ON u.id = b.user_id AND b.price > 2000
The above query should provide the following result:
id | name | id | country | user_id | price |
-------------|----------------------------------------|
1 | Alice | 2 | France | 1 | 2500 |
1 | Alice | 3 | Spain | 1 | 3000 |
2 | Bob | null | null | null | null |
Now I want to do it using JPA with Query DSL
JPA-related stuff:
#Entity
public class User {
#Id
private Long id;
private String name;
#OneToMany(cascade = ALL, fetch = EAGER, orphanRemoval = true, mappedBy = "user")
private List<Booking> bookings;
// getters and setters
}
#Entity
public class Booking {
#Id
private Long id;
private String name;
private Integer price;
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "user_id")
private User user;
// getters and setters
}
Query DSL:
public List<User> getUsersAndBookings() {
QUser user = QUser.user;
QBooking booking = QBooking.booking;
JPAQuery<User> jpaQuery = new JPAQuery(entityManager);
List<User> result = jpaQuery.from(user).leftJoin(user.bookings, booking).on(booking.price.gt(2000)).fetchJoin().fetch();
return result;
}
In fact, this code is not working and I get the following exception:
org.hibernate.hql.internal.ast.QuerySyntaxException: with-clause not allowed on fetched associations; use filters [select user from com.example.demo.entity.User user left join fetch user.bookings as booking with booking.price > ?1]
The problem is that the condition clause is specified in on method - on(booking.price.gt(2000)).
After some research I found that this condition should be specified in where method and should look like this:
List<User> result = jpaQuery.from(user).leftJoin(user.bookings, booking).where(booking.price.gt(2000)).fetchJoin().fetch();
This works, but not how I would expect it to work, since it doesn't return ALL users, it returns only one user (Alice), which has some bookings, matching the condition clause. Basically, it just filters the merged table (result table after left join operation) and that's not what I'm looking for.
I want to retrieve all users, and if there are no any bookings for a specific user, then just have null instead of booking list for this user.
Please help, been struggling for hours without any success.
Versions used:
Spring Boot 2.0.2
Spring Data JPA 2.0.7
Hibernate 5.2.16.Final
QueryDSL 4.1.4
You can use isNull expression in where clause to get the rows that have null values.
Your query should be like this:
jpaQuery.from(user)
.leftJoin(user.bookings, booking)
.fetchJoin()
.where(booking.price.gt(2000).or(booking.id.isNull())).fetch();
Hibernate produced query:
select
user0_.id as id1_1_0_,
bookings1_.id as id1_0_1_,
user0_.name as name2_1_0_,
bookings1_.country as country2_0_1_,
bookings1_.price as price3_0_1_,
bookings1_.user_id as user_id4_0_1_,
bookings1_.user_id as user_id4_0_0__,
bookings1_.id as id1_0_0__
from
user user0_
left outer join
booking bookings1_
on user0_.id=bookings1_.user_id
where
bookings1_.id is null
or bookings1_.price>?
It seems there is no JPA way for this. But I got it fixed in Hibernate way, using Filters org.hibernate.annotations.Filter.
#Entity
#FilterDef(name = "anyName", parameters = {
#ParamDef(name = "price", type = "integer")
})
public class User {
#Id
private Long id;
private String name;
#OneToMany(cascade = ALL, fetch = EAGER, orphanRemoval = true, mappedBy = "user")
#Filter(name = "anyName", condition = "price > :inputPrice")
private List<Booking> bookings;
}
Before querying the db, you must enable this filter.
Session session = enityManager.unwrap(Session.class);
session.enableFilter("anyName").setParameter("inputPrice", 2000);
// fetch using hql or criteria; but don't use booking.price.gt(2000) or similar condition there
session.disableFilter("anyName");
Now the result will have a User even if all of his booking prices are below 2000 and bookings list will be empty as expected.
NOTE: The word price in condition should be exactly same as the db column name; not as the model property name.

Use Hibernate Criteria API to return first row of each group

I am new to Hibernate and I am trying to write a criteria query to return the latest status of employee on a given date
id | Status | status_date
1 | Active | 1/10/2017
2 | Active | 1/10/2017
...
1 | Inactive| 5/10/2017
...
1 | Active | 9/10/2017
So I will be passing a date to the query and want to find the latest status of every employee on that date
The expected result will be something like this
Example: For date 6/1/2017, this will be the returned data
id | Status | Date
1 | Inactive| 5/10/2017
2 | Active | 1/10/2017
I was able to add group by with id and order the rows by status date in descending order. Is there a way I can select only the top row for each group? I tried to use the max function on status_date but that does not work.
CriteriaBuilder builder = this.entityManager.getCriteriaBuilder();
CriteriaQuery<Employee> cq = builder.createQuery(Employee);
Root<Employee> root = cq.from(Employee.class);
cq.select(root);
cq.groupBy(root.get("id"));
cq.orderBy(builder.desc(root.get("status_date")));
cq.having(builder.max(root.get("status_date")));
Since you want to output aggregation, not use aggregation as condition so you should not place it in having clause. You must add the aggregation to selection list instead.
First you must create aggregation result class (It's usual to different to your entity class):
public static class StatusEntityResult {
private String userId;
private String status;
private Date statusDate;
// getter, setter, constructor with all parameters here
}
Then create a query using it as result:
public List<StatusEntityResult> queryStatus() throws ParseException {
// Date condition
Date targetDate = new SimpleDateFormat("yyyy-MM-dd").parse("2017-10-06");
CriteriaBuilder builder = this.entityManager.getCriteriaBuilder();
// Use StatusEntityResult as result
CriteriaQuery<StatusEntityResult> cq = builder.createQuery(StatusEntityResult.class);
Root<Employee> root = cq.from(Employee.class);
// Select `id`, `status` and `max(status_date)`
cq.multiselect(root.get("id"), root.get("status"), builder.max(root.get("statusDate")))
.where(builder.lessThanOrEqualTo(root.get("statusDate"), targetDate))
.groupBy(root.get("id"))
.orderBy(builder.desc(root.get("statusDate")));
return this.entityManager.createQuery(cq).getResultList();
}
And the result is:
Side note
From what you wrote, you was attempting to use JPA Criteria, not Hibernate Criteria. For Hibernate Criteria solution, you can try to read #default locale suggestion

Hibernate Join Mapping

I have two db tables:
TARGET_TABLE (composite key on USER_ID and TARGET_ID)
USER_ID | TICKET_NO | TARGET_USER
---------------------------------
A11 | 12345 | A22
A11 | 12346 | A33
A44 | 12347 | A55
USER_DETAILS_TABLE
CORP_ID | USER_NAME
------------------
A11 | Steve
A22 | Jon
A33 | Paul
A44 | Dave
A55 | James
I want to be able to join these tables when I'm using select statements only.
For example I would like to do:
Select USER_ID, USER_NAME, TICKET_NO FROM TARGET_TABLE INNER JOIN USER_DETAILS ON TARGET_TABLE.USER_ID = USER_DETAILS_TABLE.CORP_ID
I can't seem to find the best way to do this. I have looked at the Hibernate examples for mapping but these are examples on how to write to both tables I simply want to get a user name from a table I can't touch!
I currently have two separate mappings for each table and run a separate query to get the user name but this doesn't seem like the best way to do it.
This HQL would work select tt.userId, tt.ticketNo, u.userName from TargetTable tt, User u where tt.userId = u.corpId.
However, this would return a List<Object[]> where each list element represents one row with 3 columns. You could extract these manually, or for example have some my.package.UserVO object with constructor public UserVO(String userId, String ticketNo, String userName) { ... }. In that case, this
session.createQuery("select new my.package.UserVO(tt.userId, tt.ticketNo, u.userName) from TargetTable tt, User u where tt.userId = u.corpId", UserVO.class).list()
would return instances of UserVO.

Hibernate - Distinct Query returning two columns result

Sometime back I asked the question regarding how to do a Distinct query using Hibernate. Now that I'm past that milestone, there is another thing that I require. And that is, given the table,
---------------------------------------
| user_id | user_name | user_type |
---------------------------------------
| 1 | mark taylor | admin |
| 2 | bill paxton |co-ordinator|
| 1 | tony brooks | admin |
| 3 | ali jahan | developer |
---------------------------------------
I want to create a distinct query which returns the distinct user_type along with it's corresponding user_id. Please do note that the user_id for a particular user_type is same. So for example,
admin = 1
co-ordinator = 2
developer = 3
So the return I'm expecting is somewhat like a ArrayList or that sort which contains both values like
user_id,user_type
The code I've written to get Distinct UserType is as follows and I'm hoping there could be some modification to it to get the desired result.
public List<String> findDistinctUserName() throws HibernateException {
List<String> returnVal = new ArrayList<String>();
Criteria c = this.createCriteria();
c.setProjection(Projections.distinct(Projections.property("userType")));
c.addOrder(Order.asc("userType"));
List<String> userTypeList = c.list();
for(String userType : userTypeList) {
if(!userType.equalsIgnoreCase("")) {
returnVal.add(userType);
}
}
return returnVal;
}
Thank you for your answers in advance.
Try this:
criteria.setProjection(Projections.distinct(Projections.property("userType")), "userType");
Also u don't have to check for blank strings, try this:
criteria.add(Restrictions.ne("userType",""));

Categories