JPQL left outer join on many to many relationship - jpa

I'm trying to write a query (using JPQL) to get all tags and how often each has been referenced by all items. The JPQL is:
SELECT t.id, t.name, SIZE(t.items) FROM Tag t GROUP BY t.id, t.name ORDER BY t.name ASC
Note that some tags are not referenced by any items, but I still want them included in the results (and expecting SIZE(t.items) would be zero).
However, when this query is executed, it returns only the tags that have items associated with it, ignoring tags that are not referenced by any items. The generated SQL from JPA is:
SELECT t0.id as a1, t0.NAME AS a2, COUNT(t1.id) FROM tag t0, item_tag_map t2, item t1 WHERE ((t2.tag_id = t0.id) AND (t1.id = t2.item_id)) GROUP BY t0.id, t0.NAME ORDER BY t0.NAME ASC;
It is not performing a LEFT OUTER JOIN which is required to get the results I want. If I was writing this in SQL, I would have done something like
select t.id, t.name, count(map.item_id) from tag as t left join item_tag_map as map on map.tag_id = t.id group by t.id, t.name order by t.name ASC;
Is there any way to accomplish this in JPQL? Am I doing something wrong or is it a limitation of JPQL (or a bug)? I'm using PostgreSQL v9.2 and EclipseLink v2.4.2
To provide more details... I have 3 SQL tables: item, tag, and item_tag_map. And here are the snippet of the related Java classes:
#Entity
#Table(name="item")
public class Item implements Serializable {
#Id
#Column(name="id", updatable=false)
private String id;
#ManyToMany(cascade=CascadeType.ALL)
#JoinTable(
name = "item_tag_map",
joinColumns = { #JoinColumn(name = "item_id", referencedColumnName = "id", nullable=false) },
inverseJoinColumns = { #JoinColumn(name = "tag_id", referencedColumnName = "id", nullable=false) })
private List<Tag> tags;
...
#Entity
#Table(name="tag")
#NamedQueries({
#NamedQuery(name="Tag.findAllStats", query="SELECT t.id, t.name, SIZE(t.items) FROM Tag t GROUP BY t.id, t.name ORDER BY t.name ASC"),
})
public class Tag implements Serializable {
#Id
#Column(name="id", updatable=false)
private long id;
private String name;
#ManyToMany(mappedBy="tags", fetch=FetchType.LAZY)
private List<Item> items;
...

I'm not sure if it's an EclipseLink bug or not (it looks like it is one to me, though), but you could probably use the following query to solve the problem (not tested though):
select t.id, t.name, count(i.id) from Tag t
left join t.items i
group by t.id, t.name
order by t.name asc

Related

Add custom clause to HQL join query

I have the Entity AppVisit with ManytoOne relation with User entity. User entity has OneToOne relation with UserDetail Entity.
I need to get all users having number of visits on a specific date less than their maxDailyVisits
Below are the classes
public class AppVisit{
#ManyToOne(fetch = FetchType.LAZY)
private User user;
private LocalDate visitDate;
}
public class UserDetail{
private Long id;
private int maxDailyVisits;
#OneToOne
private User user;
}
public class User {
private Long id;
#OneToMany(mappedBy = "user")
private List<AppVisit> appVisits;
#OneToOne(mappedBy = "user")
private UserDetail userDetail;
}
Here the HQL query I tried to use in the repository
select ud from UserDetail as ud where u.id in
(select ud1.id from UserDetail as ud1
left join ud1.user.appVisits as av
with acav.visitDate=:visitDate or acav.visitDate is null
Group by u1.id having Count(ud1.id) < ud1.maxDailyVisits)
At the execution time, I got the following SQL query executed to database:
select * from "UserDetail" ud
cross join "User" u where ud."User_Id"=u."Id"
and (ud."Id" in
(select ud1."Id" from "UserDetail" ud1
left outer join "User" u1 on ud1."User_Id"=u1."Id"
and (av."VisitDate"=? or av."VisitDate" is null)
left outer join "AppVisit" av
on u1."Id"=av."User_Id"
group by ud1."Id"
having count(ud1."Id")<ud1."MaxDailyVisits"))
Also I got this error:
missing FROM-clause entry for table "av"
because the condition on VisitDate column was put before the join on AppVisit Table
Any solution for this issue?
When you are using the HQL please provide a direct entity class name like below.
left join appVisits as av
Another issue is you are missing the join condition too. Full query should be like below.
select ud from UserDetail as ud where u.id in
(select ud1.id from UserDetail as ud1
left join appVisits as av on ud1.id = av.user.id
where av.visitDate=:visitDate or av.visitDate is null
Group by u1.id having Count(ud1.id) < ud1.maxDailyVisits)

How to use string_agg with PostgreSQL in #Query annotation without nativeQuery flag?

I need to remove the nativeQuery flag from the #Query annotation.
The table structure may change in the future, and code without nativeQuery will make it easier to maintain later.
I have a class Parent that is linked to class Child with the #ManyToMany annotation.
Class Child has a field pseudonym that is a value of the String type.
The result of the query needs to be sorted by the String value from class Child, which I have to sort and then concatenate into one String value.
The query without the nativeQuery flag in the #Query annotation works if I do not add an additional sort in the string_agg function:
order by string_agg(c.pseudonym, ',')
If I add additional required sorting as below, an exception occurs
order by string_agg(c.pseudonym, ',' order by c.pseudonym)
org.hibernate.hql.internal.ast.QuerySyntaxException: expecting CLOSE, found 'order' near line 1, column ...
#Entity
#Getter
#Setter
#Table(name = "parent")
public class Parent {
#Id
private Long id;
private String name;
#ManyToMany
#JoinTable(
name = "parent_child_link",
joinColumns = {#JoinColumn(name = "parent_id")},
inverseJoinColumns = {#JoinColumn(name = "child_id")}
)
#OrderBy("pseudonym ASC")
private List<Child> childs = new ArrayList<>();
}
#Entity
#Getter
#Setter
#Table(name = "child")
public class Child {
#Id
private Long id;
private String pseudonym;
#ManyToMany(mappedBy = "childs")
private Set<Parent> parents = new HashSet<>();
}
public interface ParentRepository extends JpaRepository<Parent, Long> {
#Query(nativeQuery = true, value =
"select p.*" +
" from parent p" +
" left join parent_child_link link on p.id = link.parent_id" +
" left join child c on link.child_id = c.id" +
" where p.name = :name" +
" group by (p.id)" +
" order by string_agg(c.pseudonym, ',' order by c.pseudonym)")
Page<Parent> find(#Param("name") String name, Pageable pageable);
}
Please try with nested query:
select p.*
from (
select p, string_agg(c.pseudonym, ',' order by c.pseudonym) ord
from parent p
left join parent_child_link link on p.id = link.parent_id
left join child c on link.child_id = c.id
where p.name = :name
group by (p.id)
) inn(p, ord)
order by ord
or:
select p.*
from(
select p, c.pseudonym
from parent p
left join parent_child_link link on p.id = link.parent_id
left join child c on link.child_id = c.id
where p.name = :name
order by p, pseudonym
) inn(p, pseudonym)
group by p.id
order by string_agg(pseudonym, ',')

Override column selection in jpa

I need to select an integer column (int) by left joining its entity.
When there are no rows, its value is "null", what I need is to have 0 instead of null.
I searched for an annotation or a way to override the select query with no result. Note that I'm using eclipselink.
I am searching for something that always makes the selection of the column "coalesce(columnName,0)" instead of columnName.
EDIT:
A code Sample:
public class parent{
#Id
private int id;
private String field1;
private String field2;
private List<Child> children;
}
public class Child{
#Id
private int id;
private int myNumber;
private String field;
}
The JPA Query is similar to
SELECT p FROM parent p LEFT JOIN p.children c ON c.field = 'aaa' WHERE p.field1 = 'bbb'
The translation of this query would be:
SELECT t0.id, t0.field1, t0.field2, t1.id, t1.myNumber, t1.field
FROM parent t0 LEFT OUTER JOIN child t1 on t0.id = t1.parent_id AND t1.field = 'aaaa
WHERE t0.field1 = 'bbb'
In the case where there are no rows in CHILD, the field: t1.myNumber will have the value null.
To fix it, in Sql statement, normally I will use coalesce(t1.myNumber,0). So the query will be:
SELECT t0.id, t0.field1, t0.field2, t1.id, coalesce(t1.myNumber,0), t1.field
FROM parent t0 LEFT OUTER JOIN child t1 on t0.id = t1.parent_id AND t1.field = 'aaaa
WHERE t0.field1 = 'bbb'
What I need is to accomplish this result using some annotation, or any other tool if exists, whithout changing the JPA query if possible.

Querying EXISTS in many to many relations in Criteria API

Having two entity classes with a many to many relation, I am trying to create query which tests the existence of any relationship for all entities from one table. I am stuck because it seems that there is no way to refer to the JoinTable through the Criteria API.
Example entities:
#Entity
#Table(name="man")
public class Man {
#Id
#GeneratedValue
private Long id;
}
#Entity
#Table(name="woman")
public class Woman {
#Id
#GeneratedValue
private Long id;
#ManyToMany
#JoinTable(
name="man_woman",
joinColumns=
#JoinColumn(name="woman_id", referencedColumnName="id"),
inverseJoinColumns=
#JoinColumn(name="man_id", referencedColumnName="id")
)
private Set<Man> men;
}
I would like to create a query using criteria API which would result in SQL such as:
select m.id,
case when exists(select * from man_woman mw where mw.man_id=m.id) then 1 else 0
from man;
The best I have come up with so far is the following:
CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createTupleQuery();
Root<Man> from = criteriaQuery.from(Man.class);
Subquery<Long> subquery = criteriaQuery.subquery(Long.class);
Root<Woman> sub_from = subquery.from(Woman.class);
SetJoin<Woman, Man> setJoin = sub_from.join(Woman_.men);
subquery.select(sub_from.get(Woman_.id));
subquery.where(from.in(setJoin.get(Man_.id)));
criteriaQuery.multiselect(from.alias("man_entity"),
criteriaBuilder.selectCase()
.when(
criteriaBuilder.exists(subquery)
, true)
.otherwise(false)
.alias("knows_any_women")
);
return em.createQuery(criteriaQuery).getResultList()
which results in SQL containing extra joins:
select m.id,
case when exists(select w.id
from woman w
inner join man_woman mw on w.id = mw.woman_id
inner join man m2 on m2.id = mw.man_id
where m.id in (m2.id)
)
then 1 else 0
from man;
I guess this statement would eventually be optimized to look like my desired one - but is there a way to make it simpler from the beginning?

JPA criteria api order by query for #EmbeddedId

I have 2 classes:
#Entity
#Table(name = "DRCOMMENTS" ,schema = "XXX")
#XmlRootElement
#NamedQueries({
#NamedQuery(name = "Drcomments.findAll", query = "SELECT d FROM Drcomments d"),
public class Drcomments implements Serializable {
private static final long serialVersionUID = 1L;
#EmbeddedId
protected DrcommentsPK drcommentsPK;
#Size(max = 50)
#Column(name = "SDESC")
private String commentSecondaryCodeDescription;
}
#Embeddable
public class DrcommentsPK implements Serializable {
#Column(name = "CODE")
private Short commentPrimaryCode;
#NotNull
#Column(name = "SCODE" , length=5)
private Short commentSecondaryCode;
}
I'm trying to create a query with a dynamic order by and a parameter, for example:
I want to select all Drcomments records when DrcommentsPK.commentPrimaryCode equels 1, and the order by will be by DrcommentsPK.commentSecondaryCode. this is what i tried:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Drcomments> q = cb.createQuery(Drcomments.class);
Root<Drcomments> c = q.from(Drcomments.class);
q.select(c);
q.where(cb.equal(c.get("drcommentsPK").get("commentPrimaryCode"), 1));
Path<Drcomments> valuePath = c.get("drcommentsPK").get("commentSecondaryCode");
Order[] orders;
if(sord.equals("desc"))
{
orders = new Order[] {cb.desc(valuePath)};
}
else
{
orders = new Order[] {cb.asc(valuePath)};
}
q.orderBy(orders);
query = em.createQuery(q);
query.setFirstResult(start);
query.setMaxResults(start + limit);
results = query.getResultList();
The problem is that the resultlist I get is not sorted in the commentSecondaryCode desc order..
am I doing somthing wrong? how can this be done? how to create a query that will be ordered by a field inside the emeddable class?
UPDATE:
this is the generated sql I get:
SELECT * FROM
(SELECT * FROM
(SELECT EL_TEMP.*, ROWNUMBER() OVER() AS EL_ROWNM FROM
(SELECT CMSSDESC AS a1, CMSSCODE AS a4, CMSPCODE AS a5 FROM DRCOMMENTS
WHERE (CMSPCODE = 1) ORDER BY CMSSCODE DESC, CMSPCODE DESC)
AS EL_TEMP)
AS EL_TEMP2 WHERE EL_ROWNM <= 50)
AS EL_TEMP3 WHERE EL_ROWNM > 0
when I run this code it wont return the records in the CMSSCODE desc order..
(beacuse the order by should be in the outer select..)
do I need to change somthing in the query.setFirstResult() and query.setMaxResults() ?
I how do I add it to the end in the criteria query, so it will be in the last select?
Thank's In Advance.
Your code is fine and standard compliant. If you are sure it doesn't work, then there must be bug in implementation or some odd stuff in those parts of code that is not shown. For example, check that value of sord is exactly "desc" (case sensitive), because otherwise you will fall to use ascending order.
I tried it with Hibernate 3.6.8.Final and EclipseLink (2.3.2). With both of them it works as expected - ORDER BY SCODE [ASC/DESC] is part of the executed SQL query.