I have the next entity:
#Entity
#Table(name = "search_request_items")
public class SearchRequestItem extends LongIdEntity {
#Column(name = "date")
private Instant date;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id")
private User user;
#Column(name = "result_count")
private Long resultCount;
/**
* Request's text.
*/
#Column(name = "request")
private String request;
/**
* Request's quality. It may take 0 or 1.
*/
#Column(name = "quality")
private Integer quality;
...
}
Then i have the next queryDSL query, which return collection of quality avg and user count grouped by request' text:
public JPAQuery<Tuple> prepareTotalQuery() {
QSearchRequestItem requestItem = QSearchRequestItem.searchRequestItem;
QUser user = QUser.user;
NumberExpression<Double> qualityAvgExpression = requestItem.quality.avg();
NumberExpression<Long> qualityCountExpression = requestItem.user.countDistinct();
JPAQuery<Tuple> query = queryFactory
.select(qualityAvgExpression, qualityCountExpression)
.from(requestItem)
.leftJoin(requestItem.user, user)
.groupBy(requestItem.request)
.having(qualityAvgExpression.isNotNull(),
qualityCountExpression.gt(2));
return query;
}
But i need to return total avg upon this collection just like this native query:
select avg(n1.avg_quality)
from (select count(distinct user_id), avg(quality) as avg_quality
from search_request_items
group by request
having avg(quality) is not null and count(distinct user_id) > 2
) n1;
So, how to update my querydsl query to get this result?
This issue here is that you're using JPA and JPA doesn't allow to use subqueries as join target in the from clause.
Blaze-Persistence is an extension of JPA and integrates well with Hibernate. It adds Common Table Expressions and subselect (even lateral) joins to JPQL. Blaze-Persistence also has a Querydsl integration, allowing you to write a query like the following:
List<Number> fetch = new BlazeJPAQuery<>(entityManager, cbf)
.with(cteType, new BlazeJPAQuery<>()
.bind(cteType.avgQuantity, requestItem.quality.avg())
.from(requestItem)
.leftJoin(requestItem.user, user)
.groupBy(requestItem.request)
.having(qualityAvgExpression.isNotNull(), qualityCountExpression.gt(2))))
)
.select(cteType.avgQuantity.avg())
.from(cteType)
.fetch();
However, with plain JPA and Hibernate, there is no simple way to do this.
Provided that you're only averaging a set of numbers though, which are not intensive to serialize over JDBC and do not suffer from potential N+1 issues, I'd suggest to simply do the final average step in memory:
queryFactory
.select(qualityCountExpression)
.from(requestItem)
.leftJoin(requestItem.user, user)
.groupBy(requestItem.request)
.having(qualityAvgExpression.isNotNull(),
qualityCountExpression.gt(2))
.stream()
.collect(Collectors.averagingDouble(i -> i.doubleValue()))
Related
I have 3 entities. Customer, Process and Document.
A Customer has many processes and a process has many documents.
I want to sort customers by document's updateDate.
My entities are like below;
Customer-
#Entity
public class Customer {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Process> processes = new ArrayList<>();
// getter, setter etc.
}
Process-
#Entity
public class Process {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String type;
#ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
#OneToMany(mappedBy = "process", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Document> documents = new ArrayList<>();
//getter, setter etc.
}
Document-
#Entity
public class Document {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String note;
private LocalDateTime updateDate;
#ManyToOne(fetch = FetchType.LAZY)
private Process process;
}
I have tried the following specification-
public static Specification<Customer> orderByDocumentUploadDate() {
return (root, query, criteriaBuilder) -> {
ListJoin<Customer, Process> processJoin = root.join(Customer_.processes);
ListJoin<Process, Document> documentJoin = processJoin.join(Process_.documents);
query.orderBy(criteriaBuilder.desc(documentJoin.get(Document_.updateDate)));
query.distinct(true);
return null;
};
}
It gives following error-
ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select
list
Generated SQL-
select distinct customer0_.id as id1_0_,
customer0_.name as name2_0_
from customer customer0_
inner join
process processes1_ on customer0_.id = processes1_.customer_id
inner join
document documents2_ on processes1_.id = documents2_.process_id
order by documents2_.update_date desc
limit ?
I have also tried by grouping, like below-
public static Specification<Customer> orderByDocumentUploadDate() {
return (root, query, criteriaBuilder) -> {
ListJoin<Customer, Process> processJoin = root.join(Customer_.processes);
ListJoin<Process, Document> documentJoin = processJoin.join(Process_.documents);
query.orderBy(criteriaBuilder.desc(documentJoin.get(Document_.updateDate)));
query.groupBy(root.get(Customer_.id));
return null;
};
}
Then it gave a different error-
ERROR: column "documents2_.update_date" must appear in the GROUP BY
clause or be used in an aggregate function
Generated SQL-
select
customer0_.id as id1_0_,
customer0_.name as name2_0_
from
customer customer0_
inner join
process processes1_
on customer0_.id=processes1_.customer_id
inner join
document documents2_
on processes1_.id=documents2_.process_id
group by
customer0_.id
order by
documents2_.update_date desc limit ?
I could do it by the following sql; max() solved it in below sql-
select customer.* from customer
inner join process p on customer.id = p.customer_id
inner join document d on p.id = d.process_id
group by customer.id
order by max(d.update_date);
But I can't do the same, using the criteria API.
Do you have any suggestion?
This is a conceptual misunderstanding.
First, you have to understand how does inner join works. And this portion is okay in this case: [join process table with document table based on document.process_id = process.id]
Second, you need to sort customers based on the document's update date
Unfortunately, you used group by here. GROUP BY only returns column in which it is grouped by. In this case, it will return only customer_id.
You can use aggregate functions like count(), sum() etc. on grouped data.
When you tried to access update_date, it will throw below error:
ERROR: column "documents2_.update_date" must appear in the GROUP BY clause or be used in an aggregate function
Now, how can we get rid of this?
So first we need to do join to get customer id. After getting customer id, we should group the data by the customer id and then use max() to get max_date of each group(if necessary then minimum)
SELECT
customer_id,
max(date) AS max_date
FROM
document
JOIN process ON process.id = document.process_id
GROUP BY customer_id
It will return a temporary table, that looks something like below:
customer_id
max_date
1
2020-10-24
2
2021-03-15
3
2020-09-24
4
2020-03-15
Using the temporary table, you can now sort customer_id by date
SELECT
customer_id,
max_date
FROM
(SELECT
customer_id,
max(date) AS max_date
FROM
document
JOIN process ON process.id = document.process_id
GROUP BY customer_id) AS pd
ORDER BY max_date DESC
Hope this helps.
In a spring boot + JPA applition, I have a user table and a item table with a ManyToMany relationship. The users can "follow" any Item element, in a twitter/facebook style to get the posts related to that item, get notificacions when an item change, etc.
The classes are like this, very simple way:
UserEntity.java
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Size(max = 50)
#Column(name = "username", length = 50)
private String username;
#ManyToMany
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#JoinTable(name = "user_follow_item",
joinColumns = #JoinColumn(name="user_id", referencedColumnName="id"),
inverseJoinColumns = #JoinColumn(name="follow_item_id", referencedColumnName="id"))
private Set<Items> followItems = new HashSet<>();
ItemEntity.java
#ManyToMany(mappedBy = "followItems")
private Set<User> followUsers = new HashSet<>();
The query is made with JpaRepository repository classes and exposed in spring boot throught a #RestController class and all works just fine.
The question is, in a query looking for items by id, it is possible to get if the current user (the logged user) is following the item? All the data in one single query. With this query I get the item info, but I don't know how to get if the user if following the item:
#Query("select item from Item item left join item.followUsers userFollow on userFollow.login = ?#{principal.username} where item.id = :id ")
Item itemWithCurrentUserFollows(#Param("id") Long id);
The query in native SQL is something like this (when i get data in "follow" part I know that the user is following the item) but I don't know how to realice this in a JPA query:
SELECT * FROM item left join (user user join user_follow_item follow) on user.id = follow.user_id and follow.follow_item_id = item.id and user.username = 'mockUser' where item.id = 1;
Any ideas are welcome (#Formula, a extra boolean value with this info, etc...)
Many Thanks!
I have two Entities
#Entity
#Table(name = "steps")
public class Step {
#Id
#Column(nullable = false, insertable = true, updatable = true)
private long id;
//more fields
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "result_id", referencedColumnName = "id")
private Result result;
and
#Entity
#Table(name = "results")
public class Result {
#Id
#Column(nullable = false, insertable = true, updatable = true)
private long id;
//more fields
#OneToMany(mappedBy = "result", fetch = FetchType.EAGER)
private List<Step> steps;
Now I need to get all results and their associated steps.
I will want all the steps so an eager fetch seems best, and I know all results will have steps so an inner join will be fine.
I will want to get only some more fields from results, but not all so I can maximize the use of an index, so projection will be needed.
To accomplish this I have tried two methods:
Query query = getSession().createQuery(
"select distinct result from Result as result " +
"inner join result.steps " +
"where result.monitorId = :id " +
"and result.completed between :from and :to ");
query.setLong("id", monitorId);
query.setTimestamp("from", from);
query.setTimestamp("to", to);
for (Result result : query.list()) {
result.getSteps()
//work work
}
Hibernate does a join like I wanted but when I start to iterate over the results, Hibernate logs one more select query per step I'm using.
(I also haven't found a good way for projection if I'm going this route?)
The second approach I have tried seems great so far:
Criteria criteria = getSession().createCriteria(Result.class);
criteria.setProjection(Projections.projectionList()
.add(Projections.property("completed"), "completed")
.add(Projections.property("steps"), "steps"));
criteria.add(Restrictions.eq("someId", someId));
criteria.add(Restrictions.between("completed", from, to));
criteria.setFetchMode("steps", FetchMode.JOIN);
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
criteria.setResultTransformer(Transformers.aliasToBean(Result.class));
The projections work great as long as I don't include steps. I'm guessing that has to do with it not belonging to the actual database table? When I do try with steps I get
java.lang.ArrayIndexOutOfBoundsException: 2
on criteria.list();
So my main question is, how do I best do eager fetching of some but not all columns in results + a collection of steps belonging to each result?
Help please? :)
I want to make a JPQL query with a collection of non entities. This is my Table entity :
#Entity
#Table(name = "ct_table")
public class Table {
...
#CollectionOfElements(fetch = FetchType.EAGER)
#JoinTable(name = "ct_table_result", joinColumns = #JoinColumn(name = "tableId"))
#MapKey(columns = { #Column(name = "label") })
#Column(name = "value")
private Map<String, String> tableResults;
...
then I try to make a query like this
select count(*) from table where table.tableResults['somekey'].value='somevalue'
but I get the following exception:
Cannot create element join for a collection of non-entities!
Any suggestion??
thanks for your time
EDIT:
I use JPA 1 and hibernate 3.3. Default libraries in JBoss 5
The JPA 2 spec (page 139) defines the KEY() and VALUE() functions to access the key and the value of a map-valued element collection:
select count(t.id) from Table t
where KEY(t.tableResults) = 'somekey' and VALUE(t.tableResults) = 'somevalue'
I am currently working on a project to transfer some legacy jdbc select statements over to using Hibernate and it's criteria api.
The two relevant table columns and the SQL query looks like:
-QUERIES-
primaryId
-QUERYDETAILS-
primaryId
linkedQueryId -> Foreign key references queries.primaryId
value1
value2
select *
from queries q
where q.primaryId not in (SELECT qd.linkedQueryId
FROM querydetails qd
WHERE (qd.value1 LIKE 'PROMPT%'
OR qd.value2 LIKE 'PROMPT%'));
My entity relationships look like:
#Table("queries")
public class QueryEntity{
#Id
#Column
private Long primaryId;
#OneToMany(targetEntity = QueryDetailEntity.class, mappedBy = "query", fetch = FetchType.EAGER)
private Set<QueryDetailEntities> queryDetails;
//..getters/setters..
}
#Entity
#Table(name = "queryDetails")
public class QueryDetailEntity {
#Id
#Column
private Long primaryId;
#ManyToOne(targetEntity = QueryEntity.class)
private QueryEntity query;
#Column(name="value1")
private String value1;
#Column(name="value2")
private String value2;
//..getters/setters..
}
I am attempting to utilize the criteria api in this way:
Criteria crit = sessionFactory.getCurrentSession().createCriteria(QueryEntity.class);
DetachedCriteria subQuery = DetachedCriteria.forClass(QueryDetailEntity.class);
LogicalExpression hasPrompt = Restrictions.or(Restrictions.ilike("value1", "PROMPT%"),
Restrictions.ilike("value2", "PROMPT%"));
subQuery.add(hasPrompt);
Criterion subQueryCrit = Subqueries.notIn("queryDetails", subQuery);
crit.add(subQueryCrit);
List<QueryMainEntity> entities = (List<QueryMainEntity>) crit.list();
System.out.println("# of results = " + entities.size());
I am getting a NullPointerException on the crit.list() line that looks like
Exception in thread "main" java.lang.NullPointerException
at org.hibernate.loader.criteria.CriteriaQueryTranslator.getProjectedTypes(CriteriaQueryTranslator.java:362)
at org.hibernate.criterion.SubqueryExpression.createAndSetInnerQuery(SubqueryExpression.java:153)
at org.hibernate.criterion.SubqueryExpression.toSqlString(SubqueryExpression.java:69)
at org.hibernate.loader.criteria.CriteriaQueryTranslator.getWhereCondition(CriteriaQueryTranslator.java:380)
at org.hibernate.loader.criteria.CriteriaJoinWalker.<init>(CriteriaJoinWalker.java:114)
at org.hibernate.loader.criteria.CriteriaJoinWalker.<init>(CriteriaJoinWalker.java:83)
at org.hibernate.loader.criteria.CriteriaLoader.<init>(CriteriaLoader.java:92)
at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1687)
at org.hibernate.impl.CriteriaImpl.list(CriteriaImpl.java:347)
Now, I think its pretty safe to say I'm using the Criteria Api/Detached Query Api incorrectly, but I'm not sure what the 'correct' way to do it is since the Hibernate Docs only briefly cover criteria api subqueries.
I realize this is a pretty long question, but I figure its appear to put it all the relevant aspects of the question (query I'm attempting to represent via Criteria API, tables, entities).
Give this a shot:
DetachedCriteria d = DetachedCriteria.forClass(QueryDetailEntity.class, "qd");
d.setProjection(Projections.projectionList().add(Projections.property("qd.query")));
d.add(Restrictions.or(Restrictions.like("qd.value1", "PROMPT%"), Restrictions.like("qd.value2", "PROMPT%")));
criteria = session.createCriteria(QueryEntity.class, "q");
criteria.add(Subqueries.propertyNotIn("q.primaryId", d));
criteria.list();
The use of the following are property names, not column names:
qd.query
qd.value1
qd.value2
q.primaryId
As a side note, if this is not a dynamically generated query, have you given thought to using HQL instead?