I have a Parent with a OneToMany associations with a Child Table.
I'm trying to write a query with CriteriaBuilder to restrict the results returned from the Child table.
I'm adding a Predicate, something like
cb.equal(parent.get("children").get("sex"), "MALE")
If the Parent has a son or SON and Daughter it's returning that parent but also returning all the children they have.
Hibernate fires off the first query with my predicates but the second query to get the children only uses the JoinColumn in the where clause it doesn't include
cb.equal(parent.get("children").get("sex"), "MALE").
Thoughts?
I am using a SetJoin
children = parent.joinSet("children", JoinType.LEFT)
CLARIFICATION:
public static Specification<Parent> findPlanBenefits(Integer parentId) {
return (parent, query, cb) -> {
Predicate predicates = cb.conjunction();
List<Expression<Boolean>> expressions = predicates.getExpressions();
//Parent Criteria
expressions.add(cb.equal(parent.get("parentId"), parentId));
//Children Criteria
SetJoin<Parent, Children> children = parent.joinSet("children", JoinType.LEFT);
Predicate sex = cb.equal(children.get("sex"), "MALE");
children.on(sex);
return predicates;
};
}
I am afraid, the JOIN ON does not work as you expect in your answer. JOIN ON only tells how to join, and NOT how relationships are loaded.
So, in order to solve your problem you will have to filter the children after they are loaded, or fetch manually all male children with a separate query.
In order to check how JOIN ON works, you could try also the corresponding JPQL query.
UPDATE
OP told that the JPQL queryselect p from parent p join fetch children c where p.parentId = :parentId and c.sex = "MALE" works.
The corresponding CriteriaQuery would look like:
CriteriaQuery<Parent> criteria = cb.createQuery((Class<Parent>) Parent.class);
Root<Parent> parent = criteria.from(Parent.class);
criteria.select((Selection<T>) parent);
SetJoin<Parent, Children> children = parent.joinSet("children", JoinType.LEFT);
Predicate sexPredicate = cb.equal(children.get("sex"), "MALE");
parent.fetch(children);
//parent.fetch("children");//try also this
criteria.where(sexPredicate);
When you create a JOIN (especially when property is collection type, not SingularAttribute, you have to use it to build the criteria, so use
cb.equal(children.get("sex"), "MALE").
instead of
cb.equal(parent.get("children").get("sex"), "MALE").
For the future referrence, this is from another post, that helped me (link):
Instead of parent.joinSet use fetch and then cast it to join:
Join<Parent, Children> join = (Join<Parent, Children>)parent.fetch(Parent_.children);
As mentioned in the post linked above it is not perfect solution but it saved me a lot of headaches.
Related
I have a query that I want to make Criteria Query
select u.email, st.total_amount, st.company_total from users u
join (select user_id, SUM(balance) as total_amount,SUM(company_count) as company_total from subscription s
where s.is_active = 0
group by user_id) st on u.id = st.user_id
where u.is_active = 0
order by st.company_total
I have already made 1 criteria Query
CriteriaQuery<UserImpl> innerQuery = builder.createQuery(UserImpl.class);
Root<Subscription> subscriptionRoot = innerQuery.from(Subscription.class);
innerQuery.multiselect(subscriptionRoot.get("user").get("id"), builder.sum(subscriptionRoot.get("balance")),
builder.sum(subscriptionRoot.get("companyCount")));
I don't know how to make the outer query in spring hibernate JPA. Can some help.
Defining a JOIN clause is pretty simple. You first call the from method on your CriteriaQuery object to get a Root object. In the next step, you can call the join method to define your JOIN clause.
Following is example of Docs, you can manipulate it as per your example.
You can refer Hibernate docs for the join examples. following is sample code.
CriteriaQuery<String> q = cb.createQuery(String.class);
Root<Order> order = q.from(Order.class);
q.select(order.get("shippingAddress").<String>get("state"));
CriteriaQuery<Product> q2 = cb.createQuery(Product.class);
q2.select(q2.from(Order.class)
.join("items")
.<Item,Product>join("product"));
Docs to refer
Another quick example I found is listed below:
<Y> ListJoin<X, Y> join(ListAttribute<? super X, Y> list);
Quick example (assuming Employee has list of Tasks with many-to-many relation):
CriteriaQuery<Employee> query = criteriaBuilder.createQuery(Employee.class);
Root<Employee> employee = query.from(Employee.class);
ListJoin<Employee, Task> tasks = employee.join(Employee_.tasks);
query.select(employee)
.where(criteriaBuilder.equal(tasks.get(Task_.supervisor),
employee.get(Employee_.name)));
TypedQuery<Employee> typedQuery = entityManager.createQuery(query);
List<Employee> employees = typedQuery.getResultList();
Reference: here
I have #OneToMany association between 2 entities (Entity1 To Entity2).
My sqlQueryString consists of next steps:
select ent1.*, ent2.differ_field from Entity1 as ent1 left outer join Entity2 as ent2 on ent1.item_id = ent2.item_id
Adding some subqueries and writing results to some_field2, some_field3 etc.
Execute:
Query sqlQuery = getCurrentSession().createSQLQuery(sqlQueryString)
.setResultTransformer(Transformers.aliasToBean(SomeDto.class));
List list = sqlQuery.list();
and
class SomeDto {
item_id;
some_filed1;
...
differ_field;
...
}
So the result is the List<SomeDto>
Fields which are highlighted with grey are the same.
So what I want is to group by, for example, item_id and
the List<Object> differFieldList would be as aggregation result.
class SomeDto {
...fields...
List<Object> differFieldList;
}
or something like that Map<SomeDto, List<Object>>
I can map it manually but there is a trouble:
When I use sqlQuery.setFirstResult(offset).setMaxResults(limit)
I retrieve limit count of records. But there are redundant rows. After merge I have less count actually.
Thanks in advance!
If you would like to store the query results in a collection of this class:
class SomeDto {
...fields...
List<Object> differFieldList;
}
When using sqlQuery.setFirstResult(offset).setMaxResults(n), the number of records being limited is based on the joined result set. After merging the number of records could be less than expected, and the data in List could also be incomplete.
To get the expected data set, the query needs to be broken down into two.
In first query you simply select data from Entity1
select * from Entity1
Query.setFirstResult(offset).setMaxResults(n) can be used here to limit the records you want to return. If fields from Entity2 needs to be used as condition in this query, you may use exists subquery to join to Entity2 and filter by Entity2 fields.
Once data is returned from the query, you can extract item_id and put them into a collection, and use the collection to query Entity 2:
select item_id, differ_field from Entity2 where item_id in (:itemid)
Query.setParameterList() can be used to set the item id collection returned from first query to the second query. Then you will need to manually map data returned from query 2 to data returned from query 1.
This seems verbose. If JPA #OneToMany mapping is configured between the 2 entity objects, and your query can be written in HQL (you said not possible in comment), you may let Hibernate lazy load Entity2 collection for you automatically, in which case the code can be much cleaner, but behind the scenes Hibernate may generate more query requests to DB while lazy loading the entity sitting at Many side.
The duplicated records are natural from a relational database perspective. To group projection according to Object Oriented principles, you can use a utility like this one:
public void visit(T object, EntityContext entityContext) {
Class<T> clazz = (Class<T>) object.getClass();
ClassId<T> objectClassId = new ClassId<T>(clazz, object.getId());
boolean objectVisited = entityContext.isVisited(objectClassId);
if (!objectVisited) {
entityContext.visit(objectClassId, object);
}
P parent = getParent(object);
if (parent != null) {
Class<P> parentClass = (Class<P>) parent.getClass();
ClassId<P> parentClassId = new ClassId<P>(parentClass, parent.getId());
if (!entityContext.isVisited(parentClassId)) {
setChildren(parent);
}
List<T> children = getChildren(parent);
if (!objectVisited) {
children.add(object);
}
}
}
The code is available on GitHub.
I came accross an interesting behaviour: probably criteria API puts single quotes around parameters in query.
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<SomeClass> criteriaQuery = criteriaBuilder.createQuery(SomeClass.class);
Metamodel metamodel = em.getMetamodel();
EntityType<SomeClass> entityType_ = metamodel.entity(SomeClass.class);
Root<SomeClass> root = criteriaQuery.from( SomeClass );
criteriaQuery.select(root.get(entityType_.getSingularAttribute( "someField" ));
TypedQuery<SomeClass> q = em.createQuery(criteriaQuery);
List<SomeClass> result = (List<SomeClass>) q.getResultList();
this snippet results in a list with one column which is full of with "someField" in every single cells of the column. (select 'someField' from SomeClass; <--really works )
The select accepts this behaviour by criteria, however the group by fails, says: ORA-00979: not a GROUP BY expression.
I only think about criteria does the same substitution, like in case of select.
criteriaQuery.groupBy(root.get(entityType_.getSingularAttribute("someField") ));
How can I avoid those single qoutes in my queries?
Any suggests appreciated, thank You in advance.
I have two entities Customer and Order in a one-to-many relation.
For each customer I need to count the number of associated orders and sort the results by this number.
In a native postgres query it looks like this:
select cust.id, count(order.id) from customers cust
left outer join orders order
on cust.id = order.customer_id
where .... conditions ...
group by cust.id
order by count desc;
But I must do this using CriteriaBuilder because this query is part of a larger piece of code that uses CriteriaBuilder to put in additional conditions. In Hibernate I would have probably used Projections, but I can't find anything similar in JPA.
Any help in composing the query using CriteraBuilder would be much appreciated.
Thank you in advance.
Supposing that the entity Customer has a OneToMany property like this:
#OneToMany(mappedBy = "customerId")
private Collection<Order> orders;
You can use the following query:
EntityManager em; // to be built or injected
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<Customer> customer = cq.from(Customer.class);
CollectionJoin<Customer, Order> orders = customer.join(Customer_.orders, JoinType.LEFT);
cq.select(cb.tuple(customer, cb.count(orders)));
cq.where(... add some predicates here ...);
cq.groupBy(customer.get(Customer_.id));
cq.orderBy(cb.desc(cb.count(orders)));
List<Tuple> result = em.createQuery(cq).getResultList();
for (Tuple t : result) {
Customer c = (Customer) t.get(0);
Long cnt = (Long) t.get(1);
System.out.println("Customer " + c.getName() + " has " + cnt + " orders");
}
The above approach uses Metamodel. If you don't like it, you can replace Customer_.orders with "orders" and Customer_.id with "id".
If the OneToMany property is of another type, replace CollectionJoin with the collection of the proper type (ListJoin, SetJoin, MapJoin).
Use this inside the specification
cq.orderBy(cb.desc(cb.count(orders)));
Also send PageRequest(1, 10, Sort.unsorted()). This is how I did it.
If you are passing the Sort value as unsorted and then override criteria query with your own logic of sorting on your joined entity
Student - Course : OneToMany
JPQL :
select Student from Student student, Course course where
student.name=:STUDENTNAME and (course.courseName=:COURSENAME or
course.courseDuration=courseDuration)
Let us suppose one student might have 10 courses i want to retrieve only two records having Student - Courses(2).
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Student> criteriaQuery = criteriaBuilder.createQuery(Student.class);
Root<Student> studentRoot = criteriaQuery.from(Student.class);
studentRoot.fetch("courses", JoinType.LEFT);
Predicate condition = criteriaBuilder.equal(studentRoot.get("studentName"), "someName");
//how to add course condition here.
criteriaQuery.where(condition);
I Hope your Student entity has relation with Course mapping with property name course in student entity.
Here is Criteria Query
Criteria criteria = getSession().createCriteria(Student.class);
criteria.createAlias("course", "course");
criteria.add(Restrictions.eq("name", STUDENTNAME));
Criterion courseNameCrit = Restrictions.eq("course.courseName", courseName);
Criterion courseDurationCrit = Restrictions.eq("course.courseDuration", courseDuration);
criteria.add(Restrictions.disjunction().add(courseNameCrit).add(courseDurationCrit));
List<Student> studentList = criteria.list();
There is no equivalent of JPA QL “JOIN FETCH” in Criteria API.
JOIN FETCH uses EAGER fetch irrespective of the fetch type specified in the model annotation.
If you are using criteria api, you can call the getter method of child in the dao,so that it gets loaded.