Search code examples
javahibernatesubquerycriteriaintervals

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


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

Solution

  • 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