This feels like a trivial use case for Hibernate or JPA, but I've been struggling for a couple of days to get this to work.
I have an position entity class that has latitude, longitude and updateTime fields (among others). I would like to count the number of distinct combinations of those three fields while ignoring the others. In SQL, this is trivial:
SELECT COUNT(*) FROM (SELECT DISTINCT LONGITUDE, LATITUDE, UPDATE_TIME FROM POSITION) AS TEMP;
It is important that I abstract myh database implementation from the rest of my application because different users may wish to use different database engines. (Heck I use h2 for testing and mariadb for local production...)
I have been trying to translate this SQL into Java code using either Hibernate or JPA syntax, but I cannot figure out how.
EDIT - Here is as close as I have been able to get using JPA (ref: https://en.wikibooks.org/wiki/Java_Persistence/Criteria)
public long getCountDistinctInFlightPositions() {
Session session = sessionFactory.openSession();
CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
CriteriaQuery<Tuple> innerQuery = criteriaBuilder.createTupleQuery();
Root<Position> position = innerQuery.from(Position.class);
innerQuery.multiselect(
position.get("longitude"),
position.get("latitude"),
position.get("updateTime")
);
// The method countDistinct(Expression<?>) in the type CriteriaBuilder is not applicable for the arguments (CriteriaQuery<Tuple>)
criteriaBuilder.countDistinct(innerQuery);
return 1;
}
You can do it this way:
CriteriaQuery<Long> countQuery = cb.createQuery( Long.class );
Root<Position> root = countQuery.from( Position.class );
countQuery.select( cb.count( root.get( "id" ) ) );
Subquery<Integer> subQuery = countQuery.subquery( Integer.class );
Root<Position> subRoot = subQuery.from( Position.class );
subQuery.select( cb.min( subRoot.get( "id" ) ) );
subQuery.groupBy( subRoot.get( "longitude" ),
subRoot.get( "latitude" ),
subRoot.get( "updateTime" ) );
countQuery.where( root.get( "id" ).in( subQuery ) );
Long count = entityManager.createQuery( countQuery ).getSingleResult();
This effectively generates the following SQL:
SELECT COUNT( p0.id ) FROM Position p0
WHERE p0.id IN (
SELECT MIN( p1.id )
FROM Position p1
GROUP BY p1.longitude, p1.latitude, p1.updateTime )
In a scenario where I have 3 rows and 2 of them have the same tuple of longitude, latitude, and update time, the query will return a result of 2.
Make sure you maintain a good index on [Longtitude, Latitude, UpdateTime] here so that you can take advantage of faster GROUP BY execution. The PK is already b-tree indexed so the other operations wrt COUNT/MIN should be accounted for easily by that index already.
Related
I'm relatively new to Spring JPA CriteriaQuery. Im trying to convert my old native query in my program to criteria query and haven't been successful on join query for multiple table with conditions. I need help converting native SQL query into Criteria Query for these query below :
select * from student s inner join (
select distinct on (student_id) * from class where status = 'Active' order by
student_id,date_register desc) c on s.id = c.user_id
inner join teacher t on t.subject_id = c.subject_id
where t.status = 'Active' and s.status='Active' order by s.name desc
Update :
Below code is as far as I can go cause I dont really know much. Am i in the right direction? I'm opting for Expression because i dont know how to use Join.
CriteriaQuery<Student> query = cb.createQuery(Student.class);
Root<Student> sRoot= query.from(Student.class);
query.select(sRoot);
Subquery<Integer> subquery = query.subquery(Integer.class);
Root<Class> cRoot= subquery.from(CLass.class);
Expression<Integer> max = cb.max(cRoot.get("dateRegister"));
subquery.select(max);
subquery.groupBy(cRoot.get("student"));
query.where(
cb.and(
cb.in(cRoot.get("dateRegister")).value(subquery)
)
);
Thanks in advance!
I have a problem with writing criteria in java based on sql code, we must use JSON_TABLE function and search by parameters:
select tab1.id
from TABLE1 tab1
left join
TABLE2 param_json on tab1.id = param_json.TABLE_1_ID,
json_table(
param_json.PARAMS_JSON, '$[*]'
COLUMNS (
"paramCode" VARCHAR2(4000) PATH '$.paramCode',
"displayValue" VARCHAR2(4000) PATH '$.displayValue'
)
) jt
where jt."paramCode" = 'param1' and lower(jt."displayValue") like '%value%');
CriteriaBuilder builder = getEntityManager().getCriteriaBuilder();
CriteriaQuery<Long> query = builder.createQuery(Long.class);
Root<Table1> table1Root = query.from(Table1.class);
Join<Table1, Table2> jsonEJBJoin = table1Root.join(Table1_.table2Elements);
//how add json_table function
List<Predicate> whereConditions = new LinkedList<>();
Predicate[] predicates = new Predicate[whereConditions.size()];
//how add where conditions based on json_table function
query.where(whereConditions.toArray(predicates));
query.select(table1Root.get(Table1_.id));
List<Long> resultList = getEntityManager().createQuery(query)
.setMaxResults(10)
.getResultList();
This is hard because you're using a screwdriver as a hammer. The whole point of the Criteria API is to have platform-independent code which doesn't depend on proprietary functions like json_table.
If you have database access, I think the cleanest way to resolve this is to create a view to hide all the Oracle-specific code from JPA, e.g.:
create or replace view VIEW1 (TABLE_1_ID, PARAMCODE, DISPLAYVALUE) as
select param_json.TABLE_1_ID, jt.paramCode, jt.displayValue
from TABLE2 param_json,
json_table(
param_json.PARAMS_JSON, '$[*]'
COLUMNS (
paramCode VARCHAR2(4000) PATH '$.paramCode',
displayValue VARCHAR2(4000) PATH '$.displayValue'
)
) jt;
Then you replace Table2 in your JPA code with View1.
If you can't modify the database, I think you'll have to abandon the Criteria API and use a NativeQuery with bind variable parameters, something like:
List<Long> resultList = getEntityManager().createNativeQuery("select tab1.id
from TABLE1 tab1
left join
TABLE2 param_json on tab1.id = param_json.TABLE_1_ID,
json_table(
param_json.PARAMS_JSON, '$[*]'
COLUMNS (
paramCode VARCHAR2(4000) PATH '$.paramCode',
displayValue VARCHAR2(4000) PATH '$.displayValue'
)
) jt
where jt.paramCode = :param1 and lower(jt.displayValue) like :param2))" )
.setParameter("param1", param1)
.setParameter("param2", "%" + value + "%" )
.setMaxResults(10)
.getResultList();
As a side note, I recommend never using the case-sensitive Oracle identifiers (with double quotes). Some Oracle tutorials use them, but they only make things more complicated.
I need to put
order by tabB.id desc
limit 1
in a subquery because the subquery must return a single value.
Long filter= Long.parseLong(value);
return (root, query, builder) -> {
Subquery<B> subquery = query.subquery(B.class);
Root<B> subqueryRoot = subquery.from(B.class);
Join<B,C> ss = subqueryRoot.join(B_.idC);
subquery.correlate(ss);
subquery.select(subqueryRoot.get(B_.ID_C));
subquery.where(
builder.equal(subqueryRoot.get(B_.idA),root.get(A_.id))
);
subquery.
builder.max(subqueryRoot.get(B_.id)); //first try
builder.desc(subqueryRoot.get(B_.id)); //another try
return builder.equal(subquery, filter);
};
adding "first try" and/or "another try" nothing changes in the query created, they are simply ignored and executing it I reach:
org.postgresql.util.PSQLException: ERROR: more than one row returned by a subquery used as an expression
Is there a way to take the first element in the subquery so can apply to the where query?
My situation is similar What are the alternatives to using an ORDER BY in a Subquery in the JPA Criteria API?
but I have a equals not over the id:
SELECT q.id_project FROM status q
WHERE q.status_name like 'new'
AND q.id IN (
SELECT TOP 1 sq.id from status sq
WHERE q.id_project = sq.id_project
ORDER BY sq.id DESC )
my situation is:
SELECT A.id_project FROM tabA A
WHERE A.col like 'alpha'
AND 'centauri' = (
SELECT TOP 1 B.colAA from tabB B
WHERE A.id_project = B.id_project
ORDER BY B.id DESC )
I'm trying to write an SQL query using CriteriaQuery, but I'm having a hard time doing so. This query basically gets a list of shipments and sorts them by their authorization date. This authorization date is represented as the date attribute of the first record in the status transition messages table with an initial status of 3 and a final status of 4. This is my query:
SELECT s.id
FROM shipment s
ORDER BY (SELECT min(stm.date)
FROM status_transition_message stm
WHERE stm.initial_status = 1 AND stm.final_status = 3 AND stm.shipment_id = s.id) desc;
I've tried multiple different solutions, but none have worked so far.
My current iteration is as follows:
private void sortByAuthDate(Root<ShipmentTbl> root, CriteriaQuery<?> query, CriteriaBuilder builder, ListSort sort) {
Subquery<Timestamp> authDateQuery = query.subquery(Timestamp.class);
Root<StatusTransitionMessageTbl> stmRoot = authDateQuery.from(StatusTransitionMessageTbl.class);
Predicate shipmentId = builder.equal(stmRoot.<ShipmentTbl>get("shipment").<String>get("id"), root.<String>get("id"));
Predicate initialStatus = builder.equal(stmRoot.<Integer>get("initialStatus"), 3);
Predicate finalStatus = builder.equal(stmRoot.<Integer>get("finalStatus"), 4);
// returns the authorization date for each queried shipment
authDateQuery.select(builder.least(stmRoot.<Timestamp>get("date")))
.where(builder.and(shipmentId, initialStatus, finalStatus));
Expression<Timestamp> authDate = authDateQuery.getSelection();
Order o = sort.getSortDirection() == ListSort.SortDirection.ASC ? builder.asc(authDate) : builder.desc(authDate);
query.multiselect(authDate).orderBy(o);
}
The problem with this solution is that the SQL query generated by the CriteriaQuery does not support subqueries in the ORDER BY clause, causing a parsing exception.
My CriteriaQuery-fu is not good enough to help you with that part, but you could rewrite your SQL query to this:
SELECT s.id
FROM shipment s
LEFT JOIN status_transition_message stm
ON stm.initial_status = 1 AND stm.final_status = 3 AND stm.shipment_id = s.id
GROUP BY s.id
ORDER BY min(stm.date) DESC;
To me, this quite likely seems to be a faster solution anyway than running a correlated subquery in the ORDER BY clause, especially on RDBMS with less sophisticated optimisers.
So I attempted to follow #Lukas Eder solution and reached this solution:
private void sortByAuthDate(Root<ShipmentTbl> root, CriteriaQuery<?> query, CriteriaBuilder builder, ShipmentListSort sort) {
Join<ShipmentTbl, StatusTransitionMessageTbl> shipmentStatuses = root.join("shipmentStatus", JoinType.LEFT);
Predicate initialStatus = builder.equal(shipmentStatuses.<Integer>get("initialStatus"), 1);
Predicate finalStatus = builder.equal(shipmentStatuses.<Integer>get("finalStatus"), 3);
Expression<Timestamp> authDate = builder.least(shipmentStatuses.<Timestamp>get("date"));
Order o = sort.getSortDirection() == ShipmentListSort.SortDirection.ASC ? builder.asc(authDate) : builder.desc(authDate);
shipmentStatuses.on(builder.and(initialStatus, finalStatus));
query.multiselect(authDate).groupBy(root.<String>get("id")).orderBy(o);
}
}
But now it's throwing this exception:
ERROR o.h.e.jdbc.spi.SqlExceptionHelper - ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
This happens because the query is only going to get distinct shipments later on and it's asking for the sorting column also appear in the select. The problem is I don't know how to force CriteriaQuery to keep that column in the SELECT statement. It automatically only puts in the ORDER BY.
Here's the JPQL query it's executing in my test:
select
distinct generatedAlias0
from
ShipmentTbl as generatedAlias0
left join
generatedAlias0.shipmentStatus as generatedAlias1 with ( generatedAlias1.initialStatus=:param0 )
and (
generatedAlias1.finalStatus=:param1
)
where
lower(generatedAlias0.shipmentName) like :param2
group by
generatedAlias0.id
order by
min(generatedAlias1.date) desc
and the generated SQL query:
select
distinct shipmenttb0_.id as id1_13_,
shipmenttb0_.archived_date as archived2_13_,
shipmenttb0_.auth_code as auth_cod3_13_,
shipmenttb0_.authorization_date as authoriz4_13_,
shipmenttb0_.booked_in_by_user as booked_i5_13_,
shipmenttb0_.business_channel as business6_13_,
shipmenttb0_.courier as courier7_13_,
shipmenttb0_.courier_amount as courier_8_13_,
shipmenttb0_.courier_currency as courier_9_13_,
shipmenttb0_.ship_to as ship_to39_13_,
shipmenttb0_.estimated_shipment_date as estimat10_13_,
shipmenttb0_.last_updated_date as last_up11_13_,
shipmenttb0_.measurement_unit as measure12_13_,
shipmenttb0_.original_submitted_date as origina13_13_,
shipmenttb0_.packaging_type as packagi14_13_,
shipmenttb0_.placeholder_message as placeho15_13_,
shipmenttb0_.scheduled_period_of_day as schedul16_13_,
shipmenttb0_.scheduled_shipment_date as schedul17_13_,
shipmenttb0_.ship_from as ship_fr40_13_,
shipmenttb0_.ship_origin as ship_or41_13_,
shipmenttb0_.shipment_name as shipmen18_13_,
shipmenttb0_.status as status19_13_,
shipmenttb0_.submitted_date as submitt20_13_,
shipmenttb0_.supplier_contact_email as supplie21_13_,
shipmenttb0_.supplier_contact_name as supplie22_13_,
shipmenttb0_.supplier_contact_phone_number as supplie23_13_,
shipmenttb0_.supplier_email as supplie24_13_,
shipmenttb0_.supplier_secondary_contact_email as supplie25_13_,
shipmenttb0_.supplier_secondary_contact_name as supplie26_13_,
shipmenttb0_.supplier_secondary_contact_phone_number as supplie27_13_,
shipmenttb0_.tenant as tenant28_13_,
shipmenttb0_.total_received_boxes as total_r29_13_,
shipmenttb0_.total_units as total_u30_13_,
shipmenttb0_.total_value as total_v31_13_,
shipmenttb0_.total_volume as total_v32_13_,
shipmenttb0_.total_weight as total_w33_13_,
shipmenttb0_.tracking_number as trackin34_13_,
shipmenttb0_.tt_note as tt_note35_13_,
shipmenttb0_.tt_priority as tt_prio36_13_,
shipmenttb0_.updated_by_user as updated37_13_,
shipmenttb0_.weight_unit as weight_38_13_
from
shipment shipmenttb0_
left outer join
status_transition_message shipmentst1_
on shipmenttb0_.id=shipmentst1_.shipment_id
and (
shipmentst1_.initial_status=?
and shipmentst1_.final_status=?
)
where
lower(shipmenttb0_.shipment_name) like ?
group by
shipmenttb0_.id
order by
min(shipmentst1_.date) desc limit ?
I need to create a query and I need COUNT(*) and HAVING COUNT(*) = x.
I'm using a work around that uses the CustomProjection class, that I downloaded somewhere.
This is the SQL that I try to achieve:
select count(*) as y0_, this_.ensayo_id as y1_ from Repeticiones this_
inner join Lineas linea1_ on this_.linea_id=linea1_.id
where this_.pesoKGHA>0.0 and this_.nroRepeticion=1 and linea1_.id in (18,24)
group by this_.ensayo_id
having count(*) = 2
This is the code, where I use the Projection Hibernate class:
critRepeticion.setProjection(Projections.projectionList()
.add( Projections.groupProperty("ensayo") )
.add( CustomProjections.groupByHaving("ensayo_id",Hibernate.LONG,"COUNT(ensayo_id) = "+String.valueOf(lineas.size()))
.add( Projections.rowCount() )
);
The error is:
!STACK 0
java.lang.NullPointerException
at org.hibernate.criterion.ProjectionList.toSqlString(ProjectionList.java:50)
at org.hibernate.loader.criteria.CriteriaQueryTranslator.getSelect(CriteriaQueryTranslator.java:310)
at org.hibernate.loader.criteria.CriteriaJoinWalker.<init>(CriteriaJoinWalker.java:71)
at org.hibernate.loader.criteria.CriteriaLoader.<init>(CriteriaLoader.java:67)
at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1550)
at org.hibernate.impl.CriteriaImpl.list(CriteriaImpl.java:283)
at ar.com.cse.cseagro.controller.RepeticionController.buscarEnsayo(RepeticionController.java:101)
If I comment the line with CustomProjections class, the query work, but I don't get the HAVING COUNT(*) filter in the SQL ...
Basically the query try to retrieve, in a master - detail schema, all the master records where a list of details are simultaneously present, like if you want tho know "which invoices have both products, A and B".
That why if I got 3 items in the IN clause, I need to use HAVING COUNT = 3 clause.
Any idea or suggestion?
Best regards,
I figured out the problem. I replace CusotmProjections class, with:
.add( Projections.sqlGroupProjection("ensayo_id", groupBy , alias, types));
where groupBy, alias and types are:
String groupBy = "ensayo_id" + " having " + "count(*) = " + String.valueOf(lineas.size());
String[] alias = new String[1];
Alias[0] = "ensayo_id";
Type[] types = new Type[1];
types[0] = Hibernate.INTEGER;
and the magic is on groupby String. –
If someone needs to do it in grails it would be like:
projections {
groupProperty("id")
sqlGroupProjection(...)
rowCount()
}
Where sqlGroupProjection is available since 2.2.0
/**
* Adds a sql projection to the criteria
*
* #param sql SQL projecting
* #param groupBy group by clause
* #param columnAliases List of column aliases for the projected values
* #param types List of types for the projected values
*/
protected void sqlGroupProjection(String sql, String groupBy, List<String> columnAliases, List<Type> types) {
projectionList.add(Projections.sqlGroupProjection(sql, groupBy, columnAliases.toArray(new String[columnAliases.size()]), types.toArray(new Type[types.size()])));
}
http://grepcode.com/file/repo1.maven.org/maven2/org.grails/grails-hibernate/2.2.0/grails/orm/HibernateCriteriaBuilder.java/#267
Here is my sample, it works fine, maybe useful :
My sql query :
select COLUMN1, sum(COLUMN2) from MY_TABLE group by
COLUMN1 having sum(COLUMN2) > 1000;
And Criteria would be :
Criteria criteria = getCurrentSession().createCriteria(MyTable.Class);
ProjectionList projectionList = Projections.projectionList();
projectionList.add(Projections.property("column1"), "column1");
projectionList.add(Projections.sqlGroupProjection("sum(column2) sumColumn2 ", "COLUMN1 having sum(COLUMN2) > 1000" , new String[]{"sumColumn2"}, new org.hibernate.type.Type[]{StandardBasicTypes.STRING}));
criteria.setProjection(projectionList);
criteria.List();
criteria.add(Restrictions.sqlRestriction("1=1 having count(*) = 2"));