How can i convert a from-subquery into a Hibernate Criteria statement - java

Model
I have the following model with three classes. Class A contains 1 B, and can contain 0 or more C's. Both classes B & C contain amounts that i want to sum up together inside the scope of A.
class TableA {
#Id
Id id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "table_b_id", nullable = false)
TableB tableB;
#OneToMany(mappedBy = "tableA", fetch = FetchType.LAZY)
Set<TableC> tableCs;
}
class TableB {
#Id
Id id;
#Column(name = "amount_b")
Long amountB;
}
class TableC {
#Id
Id id;
#Column(name = "amount_c", nullable = false)
Long amountC;
#JoinColumn(name = "table_a_id")
TableA tableA;
}
Question
I am tasked to calculate the composition of two values "sum(c.amount) + b.amount" and see whether they are within a defined min/max interval, e.g. [0, 100]. The result is a List of A's that comply to this condition. I'm being asked to do it in Hibernate Criteria only.
For that i have constructed a SQL statement that expresses this requirement:
select compositeSum from
(SELECT sum(tableC.amountC) + tableB.amountB as 'compositeSum'
FROM tableA A
left join tableB B on A.b_id = B.id
left join tableC C on C.a_id = A.id) SUBQUERY
where compositeSum between 0 and 100;
Attempt
Since there is an aggregate function "sum()" and also an arithmetic operation "+", i have tried using subqueries to solve the problem.
The closest i got was the following solution:
Long min = 0l;
Long max = 100l;
DetachedCriteria subquery = DetachedCriteria.forClass(TableA.class, "inner")
.createAlias("tableC", "tableC", JoinType.LEFT_OUTER_JOIN)
.createAlias("tableB", "tableB")
.setProjection(Projections.sqlProjection("coalesce(sum(amountC), 0) + amountB", new String[] {"compositeSum"}, new Type[] {StandardBasicTypes.LONG}));
Criteria criteria = session().createCriteria(TableA.class, "outer")
.add(Subqueries.ge(max, subquery))
.add(Subqueries.le(min, subquery));
Issue
The problem is that this translates into a subquery used in the where-clause.
select * from A this_
where ? >=
(select sum(amountC) + amountB from table_A tableA_
inner join table_B tableB2_ on tableA_.table_b_id=tableB2_.id
inner join table_C tableC1_ on tableA_.id=tableC1_.table_a_id)
and ? <=
(select sum(amountC) + amountB from table_A tableA_
inner join tableB tableB2_ on tableA_.table_B_id=tableB2_.id
inner join table_C tableC1_ on tableA_.id=tableC1_.table_a_id)
I wanted to build a subquery that can be fed to the from-clause of the main query. Instead i got a subquery that is fed to the where-clause. This is not what i wanted, and results in strange results.
Does anyone know whether it is possible to feed a subquery to a from-clause in Hibernate Criteria? Many thanks!
Solution
(updated)
Apparently, adding an additional filter inside the inner query that points to the outer query solved the issue. To refer to the parent query entity from the subquery, use the magic keyword "this".
Long min = 0l;
Long max = 100l;
DetachedCriteria subquery = DetachedCriteria.forClass(TableA.class, "inner")
.createAlias("tableC", "tableC", JoinType.LEFT_OUTER_JOIN)
.createAlias("tableB", "tableB")
.setProjection(Projections.sqlProjection("coalesce(sum(amountC), 0) + amountB", new String[] {"compositeSum"}, new Type[] {StandardBasicTypes.LONG}))
.add(Restrictions.eqProperty("id", "this.id"));
Criteria criteria = session().createCriteria(TableA.class, "outer")
.setProjection(Projections.property("id"))
.add(Subqueries.ge(max, subquery))
.add(Subqueries.le(min, subquery));
The logic behind adding an additional filter is that the min/max is a scalar, and you want the subquery to return another scalar, and not a list, to compare against, which would otherwise result in erratic behavior.
This then becomes a correlated subquery:
select * from A this_
where ? >=
(select sum(amountC) + amountB from table_A tableA_
inner join table_B tableB2_ on tableA_.table_b_id=tableB2_.id
inner join table_C tableC1_ on tableA_.id=tableC1_.table_a_id
where tableA_.id = this_.id)
and ? <=
(select sum(amountC) + amountB from table_A tableA_
inner join tableB tableB2_ on tableA_.table_B_id=tableB2_.id
inner join table_C tableC1_ on tableA_.id=tableC1_.table_a_id
where tableA_.id = this_.id)
This solution is a different kind of subquery, and is probably a bit slower since you are executing the subquery twice, but it does the job.
Having-clause
Note that a SQL solution would also be possible using a having-clause.
Below, you can find the conversion of the above into a having-clause.
The only problem is that Hibernate Criteria does not support having-clauses.
SELECT coalesce(sum(amountC),0) + amountB as 'compositeSum'
FROM table_A tableA
left join table_B tableB on tableA.table_b_id = tableB.id
left join table_C tableC on tableC.table_a_id = tableA.id
group by m.id
having compositeSum between 0 and 100

You don't need sub query. Simplify your query like below:
SELECT sum(tableC.amountC) + tableB.amountB as 'compositeSum'
FROM tableA A
left join tableB B on A.b_id = B.id
left join tableC C on C.a_id = A.id
where (sum(tableC.amountC) + tableB.amountB) between 0 and 100;
and now change you hibernate code accordingly.

Apparently, adding an additional filter inside the inner query that points to the outer query solved the issue. To refer to the parent query entity from the subquery, use the magic keyword "this".
Long min = 0l;
Long max = 100l;
DetachedCriteria subquery = DetachedCriteria.forClass(TableA.class, "inner")
.createAlias("tableC", "tableC", JoinType.LEFT_OUTER_JOIN)
.createAlias("tableB", "tableB")
.setProjection(Projections.sqlProjection("coalesce(sum(amountC), 0) + amountB", new String[] {"compositeSum"}, new Type[] {StandardBasicTypes.LONG}))
.add(Restrictions.eqProperty("id", "this.id"));
Criteria criteria = session().createCriteria(TableA.class, "outer")
.setProjection(Projections.property("id"))
.add(Subqueries.ge(max, subquery))
.add(Subqueries.le(min, subquery));
The logic behind adding an additional filter is that the min/max is a scalar, and you want the subquery to return another scalar, and not a list, to compare against, which would otherwise result in erratic behavior.
This then becomes a correlated subquery:
select * from A this_
where ? >=
(select sum(amountC) + amountB from table_A tableA_
inner join table_B tableB2_ on tableA_.table_b_id=tableB2_.id
inner join table_C tableC1_ on tableA_.id=tableC1_.table_a_id
where tableA_.id = this_.id)
and ? <=
(select sum(amountC) + amountB from table_A tableA_
inner join tableB tableB2_ on tableA_.table_B_id=tableB2_.id
inner join table_C tableC1_ on tableA_.id=tableC1_.table_a_id
where tableA_.id = this_.id)
Having-clause
Note that a SQL solution would also be possible using a having-clause.
Below, you can find the conversion of the above into a having-clause.
The only problem is that Hibernate Criteria does not support having-clauses.
SELECT coalesce(sum(amountC),0) + amountB as 'compositeSum'
FROM table_A tableA
left join table_B tableB on tableA.table_b_id = tableB.id
left join table_C tableC on tableC.table_a_id = tableA.id
group by m.id
having compositeSum between 0 and 100

Related

Spring Data JPA retrieve entities where root field or list entity field matches criteria

The following entity relationship exists: A 1 <-> * B (edited down for brevity)
#Entity
public class EntityA {
#Id
UUID id;
#Column
String searchText;
#OneToMany(mappedBy = "entity_a", fetch = FetchType.LAZY)
List<EntityB> listOfB;
}
#Entity
public class EntityB {
#Id
UUID id;
#Column
String searchText;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "entity_a_id")
EntityA entityA;
}
I would like to retrieve all entities (EntityA) where a specific field (searchText) matches a criteria - easy enough. Now I want to go through all linked entities (EntityB) with a field (searchText) matching the criteria as well, only returning the matching entities. This result can be in any format for eg:
Map<EntityA, List<EntityB>> (The parent EntityA, possibly matching, with matching children EntityB)
List<EntityA_EntityBSearchResult> (some custom domain model)
List<EntityA> (bad: don't use entities with not conforming to expected relationship)
There are two main constrains complicating this
The outer entity (EntityA) should stay pageable
The resulting list (of EntityB) is limited to a maximum of 10 items
What I've tried:
Building the SQL before converting to JPA Query
For simplicity the criteria is a simple like '%textToFind%' on field searchText
1. Retrieve all of EntityB where EntityA or EntityB matches criteria.
select a.*, b.* from entity_a a
inner join entity_b b on b.entity_a_id = a.id
where a.search_text like '%textToFind%' or b.search_text like '%textToFind%'
Good: Pageable on EntityA
Bad: EntityB not limited to max of 10 items. Grouping will have to be done afterwards to Map<EntityA, List<EntityB>>.
2. Retrieve all of EntityB where EntityA or EntityB matches criteria - limited to 10 of EntityB.
select * from entity_b where id in (
select limitedB.id from (
select ROW_NUMBER() OVER(PARTITION BY a.id ORDER BY b.created_date_time DESC) AS RowNumber, b.* from entity_a a
inner join entity_b b on b.entity_a_id = a.id
where a.search_text like '%textToFind%' or b.search_text like '%textToFind%') limitedB
WHERE RowNumber <= 10)
Good: EntityB limited to 10
Bad: Not pageable on EntityA
3. Retrieve all of EntityA first (where EntityA or EntityB matches criteria). Followed by retrieval of EntityB (where EntityA or EntityB matches criteria) with parent EntityA id in previous list.
select DISTINCT a.* from entity_a a
inner join entity_b b on b.entity_a_id = a.id
where a.search_text like '%textToFind%' or b.search_text like '%textToFind%'
use the result to create an EntityA id list (List<UUID> EntityA_Ids). Send this list to the next query:
select * from entity_b where id in (
select limitedB.id from (
select ROW_NUMBER() OVER(PARTITION BY a.id ORDER BY b.created_date_time DESC) AS RowNumber, b.* from entity_a a
inner join entity_b b on b.entity_a_id = a.id
where a.search_text like '%textToFind%' or b.search_text like '%textToFind%') limitedB
WHERE RowNumber <= 10) and entity_a_id in EntityA_Ids
From this result a map (Map<EntityA, List<EntityB>>) can easily be created with Collectors.groupingBy.
Good: EntityA is pageable and EntityB is limited to 10
Bad: Multiple queries

Sub-Select In hibernate criteria

I have a sql table A with column names
name, id1, id2, val1
and a table B with column names
id1, id2, key1, key2
and this is my sql query
SELECT
v1.id1,
v1.id2
FROM (
SELECT
A.id1,
A.id2,
min(val1) AS x
FROM A
JOIN B ON A.id1 = B.id1 AND A.id2 = B.id2
GROUP BY A.id1, A.id2
) AS v1
WHERE v1.x > 10
using the DetachedCriteria i was able to form the sub-query
DetachedCriteria subCriteria = DetachedCriteria.forClass(A_model.class);
subCriteria.createAlias("b", "b_model");
subCriteria.setProjection(Projections.projectionList()
.add(Projections.groupProperty("id1"))
.add(Projections.groupProperty("id2"))
.add(Projections.min("val1"),"x");
but i am facing a hard time in creating the outer query.
any suggestion how can i create the criteria for the above SQL?
Thanks in anticipation.
Sub-selects in the from clause are not supported by Hibernate for the time being. However, your query can be rewritten in a simpler and more efficient form by utilizing the HAVING clause:
SELECT A.id1, A.id2,
FROM A JOIN B ON A.id1 = B.id1 AND A.id2 = B.id2
GROUP BY A.id1, A.id2
HAVING min(val1) > 10
The above query can be easily ported to HQL or Criteria.
Consider creating a view for the data you need:
create view A_B_BY_ID1_AND_ID2 as
select A.id1,
A.id2,
min( val1 ) as x
from A
join B on A.id1 = B.id1 and A.id2 = B.id2
group by A.id1,
A.id2
Then create a DTO to represent this data:
#Entity(table="A_B_BY_ID1_AND_ID2")
#Data //are you on board with lombok?
public class ABById1AndId2 {
#Column
private int x;
#Column
private int id1;
#Column
private int id2;
}
then access it like anything else:
session.createCriteria(ABById1AndId2.class).add(Restrictions.gt("x", 10)).list();
Select from Select is neither supported by HQL nor by Criteria object. The solution here would be Named Query.
#NamedNativeQueries({
#NamedNativeQuery(
name = "findV1",
query = "SELECT
v1.id1,
v1.id2
FROM (
SELECT
A.id1,
A.id2,
min(val1) AS x
FROM A
JOIN B ON A.id1 = B.id1 AND A.id2 = B.id2
GROUP BY A.id1, A.id2
) AS v1
WHERE v1.x > 10"
)
})

Join fetch: "query specified join fetching, but the owner of the fetched association was not present in the select list"

I have a following code:
public class ValueDAO implements BusinessObject<Long> {
private Long id;
private String code;
private ClassDAO classDAO ;
....
}
public List<String> getCodesByCodeClass(Long classId) {
String select = "select distinct val.code from ValueDAO val left " +
"join fetch val.classDAO ";
String where = "where val.classDAO.id = ? order by val.code";
return getHibernateTemplate().find(select + where, classId);
}
It raises an exception:
org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list
In the result I wan to get only codes.
join fetch val.classDAO.b means "when fetching val, also fetch the classDAO linked to the val". But your query doesn't fetch val. It fetches val.code only. So the fetch makes no sense. Just remove it, and everything will be fine:
select distinct val.code from ValueDAO val
left join val.classDAO classDAO
where classDAO.id = ?
order by val.code
Some notes, though:
doing a left join and then adding a retriction like classDAO.id = ? means that the join is in fact an inner join (since classDAO can't be null and have the given ID at the same time)
naming your entities XxxDAO is very confusing. DAOs and entities are not the same thing at all.
Given the above, the query can be rewritten as
select distinct val.code from ValueDAO val
where val.classDAO.id = ?
order by val.code

Inner join with select on HQL

I want to do something like that with HQL:
SELECT *
FROM tableA a
INNER JOIN (select fieldA, sum(fieldB) as sum from tableB) b
ON a.fieldA = b.fieldA and a.fieldC = b.sum;
But this gives an error:
org.hibernate.hql.internal.ast.QuerySyntaxException: unexpected token: (...
There is any way to make this using HQL and Hibernate?
try the native SQL solution approach:
need toimport this first:
import org.hibernate.SQLQuery;
then somewhere in your code:
SQLQuery query = session.createSQLQuery(
"SELECT * FROM tableA a
INNER JOIN
(SELECT fieldA, sum(fieldB) as sum from tableB) b
ON a.fieldA = b.fieldA and a.fieldC = b.sum"
);
more on this link
and HERE (
Joins in Hibernate Query Language)
You can try to do such thing with HQL:
String sqlText =
"select entityA
from EntityA entityA, EntityB entityB
where entityA.fieldA=entityB.fieldA
and entityA.fieldC=(select sum(entityB.fieldB)
from EntityB entityB
where entityB.fieldA=entityA.fieldA)"
Query query = session.createQuery(sqlText);
It should work similar to your sql. About your statement - as I know you cannot use inner view in HQL because it is object oriented.
Here is a good article about joins in HQL.
EDIT:
According to notes from user1495181 above query can be rewritten like (but I'm not sure):
String sqlText =
"select entityA
from EntityA entityA
join entityA.entitiesB entityB
Where entityA.fieldC=(select sum(entityB.fieldB)
from EntityB entityB
where entityB.fieldA=entityA.fieldA)"
But I prefer first variant because as for me it is more understandable (especially for peoples who used to work with native SQL).
You cannot define the join with on keyword. Hibernate know how to do the join based on your mapping.
If you define a relation in the mapping between a and b than hibernate will do the join based on the relation that you defined.
If you have relation between a and b than do inner join without using on and put the join criteria in the where clause

Hibernate left join fetch - get only an ID list of the first table

I have the following HQL query which works fine, however it returns a list of full FooD objects. I only need the ID of the FooD objects as I need to have faster query. Please not that in Hibernate mappings, FooD has a many-to-one relationship with FooB.
hqlQuery = "from FooD d left join fetch d.bill where d.ts < :ts"
I have then tried to get only the ID using the same kind of HQL query:
hqlQuery = "SELECT d.id from FooD d left join fetch d.bill where d.ts < :ts"
I got a "query specified join fetching, but the owner of the fetched association was not present in the select list".
I have then converted the query to regular Oracle SQL to get only FooD.ID:
sqlQuery = "SELECT d.id from FooD d LEFT OUTER JOIN FooB b on d.foodId=b.id where d.ts < :ts"
I have then mapped FooD and FooB objects like this:
sqlQuery.addEntity(FooD.class);
sqlQuery.addEntity(FooB.class);
and then get the resulting list by calling:
hSession.createSQLQuery(sql).setTimestamp("ts", ts).list();
But got the following error: "unexpected token: on near line 1".
Does someone know how to do get only the ID of FooD when doing a left outer join on FooB using Hibernate?
Update:
I didn't test it, but this should do the trick
SELECT d.id from FooD d inner join d.bill where d.ts < :ts
when you add LEFT you make it an outer join implicitly, and there is no need to initialize bill if all you need is to join by keys
Hibernate requires the object to be in the select clause to do any eager join fetches on it
But since you have no select or where clauses on d.bill, why do you need to fetch it anyway?
If all you need is the id, why not do this, there is no reason for the redundant join:
hqlQuery = "SELECT d.id from FooD d where d.ts < :ts"

Categories