JPA Criteria API: Predicate for Exists in Another Table in Column - jpa

I need to run the following SQL in Criteria API where one additional constraint is that the main table's ned_orgunit_t.nihsac must also exist in a separate table, participating_orgs_t.nihsac.
select o.* from ned_orgunit_t o where
o.current_flag = 'Y' and
o.inactive_date is null
o.nihsac like 'HNM%' and
/* -- This is the problem -- */
exists (select nihsac from idp.participating_orgs_t where nihsac = o.nihsac);
Criteria API Java code:
// First the standard stuff, no issues: Core NED_ORGUNIT_T predicates & selection
CriteriaBuilder crbuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<NedOrgUnit> crquery = crbuilder.createQuery(NedOrgUnit.class);
Root<NedOrgUnit> root = crquery.from(NedOrgUnit.class);
crquery.select(root);
List<Predicate> predicates = new ArrayList<Predicate>();
Predicate currentFlag = crbuilder.equal(root.get("currentFlag"), "Y");
predicates.add(currentFlag);
Predicate inactiveDate = crbuilder.isNull(root.get("inactiveDate"));
predicates.add(inactiveDate);
Predicate nihsac = crbuilder.like(root.get("nihsac"), topNihsac + '%');
predicates.add(nihsac);
// Now I need to link an EXISTS match on a separate table, PARTICIPATING_ORGS_T
// ...???
// How to do it?

Related

JPA Criteria: Obtain total count just before full result with all columns; reuse Where clause

In JPA Criteria I have a complex query which works. It involves many Joins and a complex Where clause. But right before I run it for the full selection, I need to get a quick COUNT of the full resultset.
I tried to reuse my where clause and all my Joins and select from my top element, nvRoot, using cb.count. But I got the error Caused by: java.lang.IllegalStateException: No criteria query roots were specified.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Result> criteriaQuery = cb.createQuery(Result.class);
Root<NvisionTrainee> nvRoot = criteriaQuery.from(Nv.class);
Join<Object,Object> plans = nvRoot.join("plans", JoinType.LEFT);
// etc., other Joins
Predicate where = cb.conjunction();
// Complex Where clause built...
criteriaQuery.where(where);
// --- HERE I NEED TO RUN A QUICK COUNT QUERY, with all Joins/Where as built
// --- BUT THE BELOW DOESN'T WORK:
// --- Caused by: java.lang.IllegalStateException: No criteria query roots were specified
CriteriaQuery<Long> cqCount = cb.createQuery(Long.class);
cqCount.select(cb.count(nvRoot));
cqCount.distinct(true);
cqCount.where(where);
Long totalCount = entityManager.createQuery(cqCount).getSingleResult();
// --- THIS FULL QUERY WORKS (THE REMAINDER), IT GETS ME MY FULL SELECTION
CompoundSelection<Result> selectionFull = cb.construct(
Result.class,
nvRoot.get("firstName"),
// etc. - many columns
);
criteriaQuery.select(selectionFull);
criteriaQuery.distinct(true);
TypedQuery<Result> query = entityManager.createQuery(criteriaQuery);
List<Result> results = query.getResultList();
Per the comment below, I tried adding cqCount.from(Nv.class) in the code, but that gave me:
Invalid path: 'generatedAlias2.id'
The simplest workaround would be to extract the predicate-building part into a method and reuse it like so:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
//count query
CriteriaQuery<Long> cqCount = cb.createQuery(Long.class);
Root<NvisionTrainee> nvCountRoot = buildQuery(cqCount, ...);
cqCount.select(cb.count(nvCountRoot));
cqCount.distinct(true);
Long totalCount = entityManager.createQuery(cqCount).getSingleResult();
//actual query
CriteriaQuery<Result> criteriaQuery = cb.createQuery(Result.class);
Root<NvisionTrainee> nvRoot = buildQuery(criteriaQuery, ...); //you might need to return other paths created inside buildQuery if you need to use them in the SELECT clause
CompoundSelection<Result> selectionFull = cb.construct(
Result.class,
nvRoot.get("firstName"),
...
);
criteriaQuery.select(selectionFull);
criteriaQuery.distinct(true);
TypedQuery<Result> query = entityManager.createQuery(criteriaQuery);
List<Result> results = query.getResultList();
where buildQuery is defined like so:
private Root<NvisionTrainee> buildQuery(CriteriaQuery<?> query, ... /* possibly many other arguments*/) {
Root<NvisionTrainee> nvRoot = query.from(Nv.class);
Join<Object,Object> plans = nvRoot.join("plans", JoinType.LEFT);
// etc., other Joins - build your WHERE clause here
return nvRoot;
}
Aliases for roots are generated in some random manner between queries so let's hardcode them.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Result> criteriaQuery = cb.createQuery(Result.class);
Root<NvisionTrainee> nvRoot = criteriaQuery.from(Nv.class);
// -- root alias --
nvRoot.alias("nvRoot");
Join<Object,Object> plans = nvRoot.join("plans", JoinType.LEFT);
// -- root alias --
plans.alias("plansRoot");
// etc., other Joins
Predicate where = cb.conjunction();
// Complex Where clause built...
criteriaQuery.where(where);
CriteriaQuery<Long> cqCount = cb.createQuery(Long.class);
// -- Added additional roots with the same alias names --
Root<NvisionTrainee> nvRootCqCount = cqCount.from(Nv.class);
nvRootCqCount.alias("nvRoot");
Join<Object,Object> plansCqCount = nvRootCqCount.join("plans", JoinType.LEFT);
plansCqCount.alias("plansRoot");
// etc., other Joins
cqCount.select(cb.count(nvRootCqCount));
cqCount.distinct(true);
// -- and here 'where' substituted with 'criteriaQuery.getRestriction()' --
cqCount.where(criteriaQuery.getRestriction());
Long totalCount = entityManager.createQuery(cqCount).getSingleResult();
// --- THIS FULL QUERY WORKS (THE REMAINDER), IT GETS ME MY FULL SELECTION
CompoundSelection<Result> selectionFull = cb.construct(
Result.class,
nvRoot.get("firstName"),
// etc. - many columns
);
criteriaQuery.select(selectionFull);
criteriaQuery.distinct(true);
TypedQuery<Result> query = entityManager.createQuery(criteriaQuery);
List<Result> results = query.getResultList();
Written by hand so I'm not sure if this works. I have had similar problem with error: Invalid path: 'generatedAlias2.id'.

Creating Criteria By And or Or on Runtime by JPA Repository

I need to create a query by deciding on runtime. Basically I have few parameters and one of them will specify If I should use And or Or to combine criterias. By using Spring JPA Repository how can I do it? It is easy to do it for parameters as :
#Query(value = "SELECT u FROM User u WHERE u.name IN :names")
List<User> findUserByNameList(#Param("names") Collection<String> names);
But if I want to add one more criteria lets say size, and I need to decide which one of (AND,OR) to use to combine criteria, how can I do it?
Example:
SELECT u FROM User u WHERE u.name IN :names OR/AND size = 10;
public List<User> getUsers(List<String> names, Integer size, boolean useOrPredicate) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<User> criteriaQuery = criteriaBuilder.createQuery(User.class);
Root<User> user = criteriaQuery.from(User.class);
Predicate predicate1 = user.get("name").in(names);
Predicate predicate2 = criteriaBuilder.equal(user.get("size"), size);
Predicate predicate;
if(useOrPredicate)
predicate = criteriaBuilder.or(predicate1, predicate2);
else
predicate = criteriaBuilder.and(predicate1, predicate2);
criteriaQuery.where(predicate);
return entityManager.createQuery(criteriaQuery).getResultList();
}

Rewrite query in JPA

I want to rewrite this SQL query in JPA.
String hql = "SELECT date(created_at) AS cdate, sum(amount) AS amount, count(id) AS nooftransaction "
+ "FROM payment_transactions WHERE date(created_at)>=date(now()- interval 10 DAY) "
+ "AND date(created_at)<date(now()) GROUP BY date(created_at)";
TypedQuery<Merchants> query = entityManager.createQuery(hql, Merchants.class);
List<Merchants> merchants = query.getResultList();
Is there a way to rewrite the queries into JPA or I should use it as it is?
In situations like these, more often than not the best approach is to write a plain SQL view:
CREATE OR REPLACE VIEW payment_transactions_stats AS
SELECT date(created_at) AS cdate, sum(amount) AS amount, count(id) AS nooftransaction
FROM payment_transactions
WHERE date(created_at)>=date(now()- interval 10 DAY)
AND date(created_at)<date(now()) GROUP BY date(created_at);
And map it to an #Immutable entity. This approach works well when:
you have read only data
the view does not need parameters (in this case there are solutions as well which span from hacky to nice)
You provide no details about the classes and entities but it could be something like:
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> query = builder.createTupleQuery();
From<PaymentTransaction> tx = query.from(PaymentTransaction.class);
Expression<Long> sumAmount = builder.sum(tx.get("amount"));
Expression<Long> count = builder.count(tx.get("id"));
Expression<Date> createdAt = tx.get("created_at");
query.multiselect(createdAt, sumAmount, count);
query.where(builder.greaterThanOrEqualTo(createdAt, builder.function("DATEADD", "DAY", new Date(), builder.literal(-10))),
builder.lessThan(createdAt, new Date()));
query.groupBy(createdAt);
entityManager.createQuery(query).getResultList().stream()
.map(t -> new Merchants(t.get(0, Date.class), t.get(1, Long.class), t.get(2, Long.class)))
.collect(Collectors.toList());
It is better not to use JPA for complex queries like this. JPA are usually used for simple queries.
Since the question is tagged with spring-data-jpa, you could try using a Spring CRUDRepository on top of your table. In the CRUDRepository, write a custom method with the #Query annotation.
It's hard for me to formulate the entire query because I don't know the members of your Merchants class.
Alternatively you can set the nativeQuery = true for the #Query annotation and use actual DB query to solve your problem.
You can use below code
CriteriaBuilder qb = entityManager.getCriteriaBuilder();
CriteriaQuery cq = qb.createQuery();
Root paymentInstructionsRoot = cq.from(PaymentInstructions.class);
List<Predicate> predicates = new ArrayList<>();
predicates.add(qb.greaterThanOrEqualTo(path, fromDateRange));
predicates.add(qb.lessThanOrEqualTo(path, toDateRange));
Selection cdate = paymentInstructionsRoot.get(PaymentInstructions_.createdAt).alias("cdate");
Selection amount = qb.sum(paymentInstructionsRoot.get(PaymentInstructions_.amount))).alias("amount");
Selection nooftransaction = qb.count(paymentInstructionsRoot.get(PaymentInstructions_.id))).alias("nooftransaction");
Selection[] selectionExpression = {cdate, amount, nooftransaction};
Expression[] groupByExpression = {paymentInstructionsRoot.get(PaymentInstructions_.createdAt)};
cq.multiselect(selectionExpression).where(predicates.toArray(new Predicate[]{})).groupBy(groupByExpression).where(predicates.toArray(new Predicate[]{}));
List<PaymentInstructions> paymentInstructions = entityManager.createQuery(cq).getResultList();
In your Entity class that represents the 'payment_transactions' table, add the following:
#SqlResultSetMapping(
name = "PaymentTransaction.summaryMapping",
classes = {
#ConstructorResult(targetClass = PaymentTransactionSummary.class,
columns = {
#ColumnResult(name = "cdate")
, #ColumnResult(name = "amount")
, #ColumnResult(name = "nooftransaction")
})
}
)
Create a new pojo class named PaymentTransactionSummary (must match the name used above, or whatever name you choose, with member fields cdate, amount, and nooftransaction. Include a constructor that includes those three fields in the order listed above.
Then in your dao class, write this:
Query q = entityManager.createNativeQuery("your query string from above"
, "PaymentTransaction.summaryMapping");
List<PaymentTransactionSummary> results = q.getResultList();

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;
}

JPA 2.0, Criteria API, Subqueries, In Expressions

I have tried to write a query statement with a subquery and an IN expression for many times. But I have never succeeded.
I always get the exception, " Syntax error near keyword 'IN' ", the query statement was build like this,
SELECT t0.ID, t0.NAME
FROM EMPLOYEE t0
WHERE IN (SELECT ?
FROM PROJECT t2, EMPLOYEE t1
WHERE ((t2.NAME = ?) AND (t1.ID = t2.project)))
I know the word before 'IN' lose.
Have you ever written such a query? Any suggestion?
Below is the pseudo-code for using sub-query using Criteria API.
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
Root<EMPLOYEE> from = criteriaQuery.from(EMPLOYEE.class);
Path<Object> path = from.get("compare_field"); // field to map with sub-query
from.fetch("name");
from.fetch("id");
CriteriaQuery<Object> select = criteriaQuery.select(from);
Subquery<PROJECT> subquery = criteriaQuery.subquery(PROJECT.class);
Root fromProject = subquery.from(PROJECT.class);
subquery.select(fromProject.get("requiredColumnName")); // field to map with main-query
subquery.where(criteriaBuilder.and(criteriaBuilder.equal("name",name_value),criteriaBuilder.equal("id",id_value)));
select.where(criteriaBuilder.in(path).value(subquery));
TypedQuery<Object> typedQuery = entityManager.createQuery(select);
List<Object> resultList = typedQuery.getResultList();
Also it definitely needs some modification as I have tried to map it according to your query. Here is a link http://www.ibm.com/developerworks/java/library/j-typesafejpa/ which explains concept nicely.
Late resurrection.
Your query seems very similar to the one at page 259 of the book Pro JPA 2:
Mastering the Java Persistence API, which in JPQL reads:
SELECT e
FROM Employee e
WHERE e IN (SELECT emp
FROM Project p JOIN p.employees emp
WHERE p.name = :project)
Using EclipseLink + H2 database, I couldn't get neither the book's JPQL nor the respective criteria working. For this particular problem I have found that if you reference the id directly instead of letting the persistence provider figure it out everything works as expected:
SELECT e
FROM Employee e
WHERE e.id IN (SELECT emp.id
FROM Project p JOIN p.employees emp
WHERE p.name = :project)
Finally, in order to address your question, here is an equivalent strongly typed criteria query that works:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Employee> c = cb.createQuery(Employee.class);
Root<Employee> emp = c.from(Employee.class);
Subquery<Integer> sq = c.subquery(Integer.class);
Root<Project> project = sq.from(Project.class);
Join<Project, Employee> sqEmp = project.join(Project_.employees);
sq.select(sqEmp.get(Employee_.id)).where(
cb.equal(project.get(Project_.name),
cb.parameter(String.class, "project")));
c.select(emp).where(
cb.in(emp.get(Employee_.id)).value(sq));
TypedQuery<Employee> q = em.createQuery(c);
q.setParameter("project", projectName); // projectName is a String
List<Employee> employees = q.getResultList();
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Employee> criteriaQuery = criteriaBuilder.createQuery(Employee.class);
Root<Employee> empleoyeeRoot = criteriaQuery.from(Employee.class);
Subquery<Project> projectSubquery = criteriaQuery.subquery(Project.class);
Root<Project> projectRoot = projectSubquery.from(Project.class);
projectSubquery.select(projectRoot);
Expression<String> stringExpression = empleoyeeRoot.get(Employee_.ID);
Predicate predicateIn = stringExpression.in(projectSubquery);
criteriaQuery.select(criteriaBuilder.count(empleoyeeRoot)).where(predicateIn);
You can use double join, if table A B are connected only by table AB.
public static Specification<A> findB(String input) {
return (Specification<A>) (root, cq, cb) -> {
Join<A,AB> AjoinAB = root.joinList(A_.AB_LIST,JoinType.LEFT);
Join<AB,B> ABjoinB = AjoinAB.join(AB_.B,JoinType.LEFT);
return cb.equal(ABjoinB.get(B_.NAME),input);
};
}
That's just an another option
Sorry for that timing but I have came across this question and I also wanted to make SELECT IN but I didn't even thought about double join.
I hope it will help someone.