Spring Data Specification + subqueries - spring-data-jpa

I have a Measurements table like this:
Measurements
___________________________________________________________________________________________________
| id | created_at | person_id | temperature |..... several columns with other values .... |
I want to use Spring Data Specification to query this table with dynamic list of search criteria. The challenge is that I want to get only the latest entries for each person_id that meet the search criteria. The query to get the latest results for each person can be something like this:
select *
from Measurements m
inner join (
select person_id, max(created_at) as MaxDate
from Measurements
group by person_id
) tm on p.person_id = tm.person_id and p.created_at = tm.MaxDate
How can I incorporate similar subquery to Specification with predicates for search criteria.
public static Specification<Measurements> findByCriteria(final SearchCriteria searchCriteria) {
return new Specification<Measurements >() {
private static final long serialVersionUID = 1L;
#Override
public Predicate toPredicate(Root<Patient> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<>();
if(searchCriteria.getTempMinLimit() != null) {
predicates.add(cb.greaterThanOrEqualTo(root.get("temperature"), searchCriteria.getTempMinLimit()));
}
/*
other predicates
*/
OK so I have found that subquery can be placed only to the WHERE part of the query. I had to divide it into two queries in the end.
I am still confused about the toPredicate method parameters (public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {).
When I tried to call select on the query parameter nothing happened. Can I use the query parameter directly or I have to create a new query intance with the CriteriaBuilder?

Related

JPA Criteria subquery with tuple

I define two Entity
#Entity
class Template {
#Id
private String id;
private String name;
#OneToMany(fetch=FetchType.LAZY, mappedBy="template")
private List<Edition> editions;
}
class Edition {
#Id
private String id;
private Integer version;
private String state;
#ManyToOne(fetch=FetchType.LAZY)
private Template template;
}
And I want to query each template's newest version and edition's state, so my native sql is:
select
a.id, a.name, b.version, b.state
from
tb_pm_template a,
tb_pm_edition b
where
a.id = b.template_id and
(b.template_id, b.version) in (select template_id, max(version) from tb_pm_edition group by template_id)
The navtive sql work fine. But I want to write in jpa Criteria api way.
So, I try the code below:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> maxVersionQuery = cb.createTupleQuery();
Root<Edition> editionRoot = maxVersionQuery.from(Edition.class);
maxVersionQuery.multiselect(
editionRoot.get("template").get("id").alias("templateId"),
cb.max(editionRoot.get("version")).alias("maxVersion")
).groupBy(editionRoot.get("template").get("id"));
List<Tuple> maxVersion = entityManager.createQuery(maxVersionQuery).getResultList();
List<Map<String, Object>> maxVersionResult = new ArrayList<>(maxVersion.size());
for (Tuple tuple: maxVersion) {
Map<String, Object> row = new HashMap<>(2);
for (TupleElement element: tuple.getElements()) {
row.put(element.getAlias(), tuple.get(element.getAlias()));
}
maxVersionResult.add(row);
}
// the maxVersion or maxVersionResult contain the template's newest version info I want, then I want to combine the version state and template name
CriteriaQuery<Tuple> templateQuery = cb.createTupleQuery();
Root<Edition> editionRoot1 = templateQuery.from(Edition.class);
templateQuery.multiselect(
editionRoot1.get("template").get("id").alias("id"),
editionRoot1.get("template").get("name").alias("name"),
editionRoot1.get("version").alias("version"),
editionRoot1.get("state").alias("versioinState")
).where(
// here I don't know how to connect the conditions
// I try the cb.in, but it needs Expression type
// I also try to use the Subquery api, but since I need the subquery return template_id and version, so I define Subquery<Tuple>, but the Subquery instance's select method only take one parameter
// I check the official document in comments, the example only show the aggregate without groupby
);
Is my implementation way wrong? On this basis, I also need to add paging and sorting, so if it is divided into two or more SQL statements, will it affect the paging count?

CriteriaBuilder query with sub-query, single column result, max function and generic types using metamodel

My objective is to replace an old JPQL query with a generic type-safe helper method using javax.persistence.metamodel and javax.persitence.criteria.
The query is essentially
select * from table
where field1 = arg1
and field2 = arg2
and field3 = (select max (field3)
from table
where field1 = arg1
and field2 = arg2
and field3 <= arg3
)
Admittedly this is maybe too specialized a query to generalize but I see the need for 2 or 3 other more generic helpers which I can model on this solution.
I have been googling the Criteria documentation (one problem is it's easy to surf a google search result list and mistakenly move from a javax.persitence page to a JBoss Hibernate page... and they are NOT the same).
I have obviously not found a one-stop shop that tells me all I need to know:
how to select a single field in a CriteriaQuery
how to structure a subquery in a CriteriaQuery Expression
how to write a max aggregate function call using CriteriaBuilder
how to properly use Static Metamodel attributes to specify generic classes in a CriteriaBuilder query, when the table being queried has a composite key which is mapped by composite key class (using #EmbeddedId)
OK. I already had the answer before I posted the question but I thought it might be useful to publish what I found.
The use case is a CHARGE table that provides CHG_NU values for ranges of product-option-level values. The appropriate
CHG_NU from the table is the one that matches a PROD_CD and OPTION_TYPE and does not exceed the OPTION_LEVEL.
Here's the method I ended up writing (the comments are specific to the above use-case but the code is generic):
public static <X, KT, PT, BT, NT extends Number> X findWithUpperLimit (Class<X> rootClass, Class<NT> numericClass,
SingularAttribute<X, KT> keyAttr,
SingularAttribute<KT, PT> arg1Attr, PT arg1Val,
SingularAttribute<KT, BT> arg2Attr, BT arg2Val,
SingularAttribute<KT, NT> numericAttr, NT number,
EntityManager em)
{
List<X> results;
CriteriaBuilder cb = em.getCriteriaBuilder ();
// set up the query (returns a full record of the CHARGE table)...
CriteriaQuery<X> cq = cb.createQuery (rootClass);
// ... and the subquery (returns only the BigDecimal OPT_LEVEL)
Subquery<NT> sq = cq.subquery (numericClass);
// set up the root objects for the CHARGE table. Both the query and the subquery are on the same table
Root<X> root = cq.from (rootClass);
Root<X> sqRoot = sq.from (rootClass);
// the query objects and the criteria builder are used to structure the query,
// the root objects are used to get metadata from the table to assign table elements to the criteria
// the subquery gets the closest optLevel to the passed-in number...
sq.select (cb.max (sqRoot.get (keyAttr).get (numericAttr)))
.where (cb.and
(cb.equal (sqRoot.get (keyAttr).get (arg1Attr), arg1Val),
cb.equal (sqRoot.get (keyAttr).get (arg2Attr), arg2Val),
cb.le (sqRoot.get (keyAttr).get (numericAttr), number)
));
// ...and the main query matches the passed-in prodCd, optType and the optLevel found by the subquery.
cq.select (root).where (cb.and (cb.equal (root.get (keyAttr).get (arg1Attr), arg1Val),
cb.equal (root.get (keyAttr).get (arg2Attr), arg2Val),
cb.equal (root.get (keyAttr).get (numericAttr), sq)
));
results = em.createQuery (cq).getResultList ();
return results.size() == 0 ? null : results.get (0);
}
This is a code snippet that calls it:
Charge charge = DAOHelper.findWithUpperLimit (Charge.class, BigDecimal.class,
Charge_.key,
ChargeKey_.prodCd, invoice.getCharge().getChargeKey().getProdCd(),
ChargeKey_.optType, invoice.getCharge().getChargeKey().getOptType(),
ChargeKey_.optLevel, invoice.getCharge().getChargeKey().getOptType(),
em);
and here's the SQL that it generates:
select charge0_.OPTION_TYPE_CD as OPTION_1_50_,
charge0_.OPTION_LEVEL as OPTION_LEV2_50_,
charge0_.PROD_CD as PROD_CD3_50_,
charge0_.CHG_NU as CHG_NU4_50_
from CHARGE charge0_
where charge0_.PROD_CD=?
and charge0_.OPTION_TYPE_CD=?
and charge0_.OPTION_LEVEL=(select max(charge1_.OPTION_LEVEL)
from CHARGE charge1_
where charge1_.PROD_CD=?
and charge1_.OPTION_TYPE_CD=?
and charge1_.OPTION_LEVEL<=1358.00
)

How can I write an EXISTS predicate on a collection attribute in Criteria API?

I have these classes:
#Entity
public class Customer {
#Id long id;
String name;
#OneToMany List<Customer> related;
}
and I'm using this JPQL query:
select c from Customer c where c.name = 'ACME'
or exists( select 1 from c.related r where r.name = 'ACME' )
How can I write the same query with the Criteria API? I need to use exists with a subquery, like the JPQL, but I don't know how to create a subquery from a collection attribute in the Criteria API.
Something like this would give EXISTS (subquery)
Subquery<Long> sq = cq.subquery(Long.class);
Root<Customer> customerSub = sq.correlate(customer);
Join<Customer,Customer> related = customerSub.join(Customer_.related);
... extra config of subquery
Predicate existsCustomer = cb.exists(sq);
where cq is the CriteriaQuery, and cb is CriteriaBuilder. This comes from an example in the JPA 2.1 spec p323 Example 4

How replace native order by clause on JPA equivalent?

I use JPA 2.0 criteria builder. I need get data from one table and sort them by column from other. This tables have relations OneToMany:
class Club{
#OneToMany(mappedBy = "club")
private List<Address> addresses;
...
}
class Address{
#JoinColumn(name = "club_id", referencedColumnName = "id")
#ManyToOne(fetch = FetchType.LAZY)
private Club club;
#Column(name = "type")
private Long type;
#Column(name = "full_address")
private String full_address;
...
}
May be several address of some type but I need only one row of this specific address.
I write native queries with subquery, but it's has problem because subquery doesn't use in order clause and in select clause in JPA 2.0.
select c.full_name from club c
ORDER BY (select a.full_address from address a WHERE c.id= a.club_id and a.type=1 LIMIT 1)
select c.full_name, (select a.full_address from address a WHERE a.type=1 AND c.id=a.club_id LIMIT 1) as full_address FROM club c
ORDER BY fullAddress;
How I can replace native order by clause on JPA equivalent?
Thanks!
This native query also resolve problem and it can replace by JPA query
select c.full_name, min(a.full_address) FROM club c LEFT JOIN address a on c.id = a.club_id
where a.id is null or a.type=1 or not exists(SELECT 1 from address aSub WHERE aSub .club_id=c.id AND aSub.type=1)
GROUP BY c.id, c.full_name ORDER BY min(a.full_address);
JPA equivalent
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<ClubItem> query = builder.createQuery(ClubItem.class);
Root<Club> root = query.from(Club.class);
Join<Club, Address> addressJoin = root.join(Club_.address, JoinType.LEFT);
query.select(builder.construct(ClubItem.class, root.get(Club_.id), root.get(Club_.fullName), builder.function("min", String.class, addressJoin.get(Address_.fullAddress))));
Subquery<Address> subquery = query.subquery(Address.class);
Root<Address> addressRoot = subquery.from(Address.class);
subquery.select(addressRoot);
subquery.where(
builder.and(
builder.equal(addressRoot.get(Address_.type), 1),
builder.equal(addressRoot.get(Address_.clubId), root.get(Club_.id))));
query.where(builder.or(builder.isNull(addressJoin), builder.equal(addressJoin.get(Address_.type), builder.literal(new Long(1))),
builder.not(builder.exists(subquery))));
query.groupBy(root.get(Club_.id), root.get(Club_.fullName))
Order order = builder.asc(builder.function("min", String.class, addressJoin.get(Address_.fullAddress)));
query.orderBy(order);
TypedQuery<ClubItem> contentQuery = em.createQuery(query);
It's not terribly elegant, but it gets the job done...
Make your "Club" class implement Comparable. Put the order-by logic into the Comparable. Then use Collections.sort(unsortedList) to get the list into sorted form. There's also a Collections.sort(unsortedList, Comparable) method which could be useful, especially if you are doing a bunch of similar methods that just vary on order-by.

How do I count the number of rows returned by subquery?

I want to do something like this:
select count(*) from (select ...)
(As it would be in SQL), but in JPA.
Any ideas on how I would do it?
I stumbled upon this issue as well. I would ultimately like to execute the following JPQL:
SELECT COUNT(u)
FROM (
SELECT DISTINCT u
FROM User u
JOIN u.roles r
WHERE r.id IN (1)
)
But this wasn't possible, also not with criteria API. Research taught that this was just a design limitation in JPA. The JPA spec states that subqueries are only supported in WHERE and HAVING clauses (and thus not in the FROM).
Rewriting the query in the following JPQL form:
SELECT COUNT(u)
FROM User u
WHERE u IN (
SELECT DISTINCT u
FROM User u
JOIN u.roles r
WHERE r.id IN (1)
)
using the JPA Criteria API like as follows:
CriteriaQuery<Long> query = cb.createQuery(Long.class);
Root<User> u = query.from(User.class);
Subquery<User> subquery = query.subquery(User.class);
Root<User> u_ = subquery.from(User.class);
subquery.select(u_).distinct(true).where(u_.join("roles").get("id").in(Arrays.asList(1L)));
query.select(cb.count(u)).where(cb.in(u).value(subquery));
Long count = entityManager.createQuery(query).getSingleResult();
// ...
has solved the functional requirement for me. This should also give you sufficient insight into solving your particular functional requirement.
This should do the trick (If you want to use JPA criteria API):
CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
CriteriaQuery<Long> query = cb.createQuery(Long.class);
Root<Entity> root = query.from(Entity.class);
//Selecting the count
query.select(cb.count(root));
//Create your search criteria
Criteria criteria = ...
//Adding search criteria
query.where(criteria);
Long count = getEntityManager().createQuery(query).getSingleResult();
On the other hand, if you want to use JP-QL, the following code should do the trick:
//Add the where condition to the end of the query
Query query = getEntityManager().createQuery("select count(*) from Entity entity where...")
Long count = query.getSingleResult();
Use the following snippet to count rows for a given Criteria Query:
public static Query createNativeCountQuery(EntityManager em, CriteriaQuery<?> criteriaQuery) {
org.hibernate.query.Query<?> hibernateQuery = em.createQuery(criteriaQuery).unwrap(org.hibernate.query.Query.class);
String hqlQuery = hibernateQuery.getQueryString();
QueryTranslatorFactory queryTranslatorFactory = new ASTQueryTranslatorFactory();
QueryTranslator queryTranslator = queryTranslatorFactory.createQueryTranslator(
hqlQuery,
hqlQuery,
Collections.emptyMap(),
em.getEntityManagerFactory().unwrap(SessionFactoryImplementor.class),
null
);
queryTranslator.compile(Collections.emptyMap(), false);
String sqlCountQueryTemplate = "select count(*) from (%s)";
String sqlCountQuery = String.format(sqlCountQueryTemplate, queryTranslator.getSQLString());
Query nativeCountQuery = em.createNativeQuery(sqlCountQuery);
Map<Integer, Object> positionalParamBindings = getPositionalParamBindingsFromNamedParams(hibernateQuery);
positionalParamBindings.forEach(nativeCountQuery::setParameter);
return nativeCountQuery;
}
private static Map<Integer, Object> getPositionalParamBindingsFromNamedParams(org.hibernate.query.Query<?> hibernateQuery) {
Map<Integer, Object> bindings = new HashMap<>();
for (var namedParam : hibernateQuery.getParameterMetadata().getNamedParameters()) {
for (int location : namedParam.getSourceLocations()) {
bindings.put(location + 1, hibernateQuery.getParameterValue(namedParam.getName()));
}
}
return bindings;
}