Querying EXISTS in many to many relations in Criteria API - jpa

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?

Related

How to use JPQL join on unidirectional one to many relationship

Here are my entities:
#Entity
public class Author {
#Id
private Long id;
//...
}
and
#Entity
public class Book {
#Id
private Long id;
#ManyToOne(optional = true)
#JoinColumn(name = COLUMN_AUTHOR_ID, referencedColumnName = "id")
private Author author;
//...
}
I don't want to declare the Set<Book> books field in the Author entity to avoid unnecessary data fetch. I already know about fetch = FetchType.LAZY, but I have some cases in which even the lazy fetching is triggered.
Here is my question: How can I use JPQL join to retrieve the relationship in a custom query?
Look at ??books?? in the below query:
entityManager.createQuery("SELECT new " + AuthorWithBooks.class.getName() +
"(a.id, ..., group_concat(b.name)) FROM Author a LEFT JOIN a.??books?? b GROUP BY a.id", AuthorWithBooks.class);
You should consider that there might be some Author with no book and I want to include them in my query! So I can not start my join from Book.
Thanks
Simply switch the from and join clause:
entityManager.createQuery("SELECT new " + AuthorWithBooks.class.getName() +
"(a.id, ..., group_concat(b.name)) FROM Book b " +
"RIGHT JOIN b.author a GROUP BY a.id", AuthorWithBooks.class);

EclipseLink Batch Fetch Hint Not Working For Two Fields

I'm using QueryHints in Spring Data JPA to use EclipseLink Batch Fetch with a type of IN. Ultimately, I need to use this around 30 fields but it doesn't seem to work right for 2 fields. Field A has a ManyToOne relationship and Field B has a ManyToMany. Based on the results of the initial query, I would expect the batch hint to generate an IN clause with 2 ids for Field A and 12 for Field B. This works fine when the hint is turned on for one field at a time. When it is enabled for both fields, the hint only applies to whichever field is the last hint in the list of QueryHints. I've tried EAGER and LAZY fetch on the fields as a shot in the dark, but it had not impact.
Is there a limitation with mixing batch fetch hints based on the relationship type? Is there something different going on? The EclipseLink documentation isn't very detailed for this feature.
EDIT: It seems it doesn't matter what fields I enable it only, it only works for one at at time. Here is sample code for two entities. The BaseEntity defines the PK id generation.
#Entity
#Table(name = "MainEntity")
public class MainEntity extends BaseEntity implements Cloneable {
...
#ManyToMany(fetch=FetchType.LAZY, cascade=CascadeType.PERSIST)
#JoinTable(
name="EntityBMapping",
joinColumns={#JoinColumn(name="mainId", referencedColumnName="id")},
inverseJoinColumns={#JoinColumn(name="bId", referencedColumnName="id")})
#JsonIgnore
private Set<EntityB> bSet = new HashSet<>();
#ManyToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
#JoinTable(
name="EntityAMapping",
joinColumns={#JoinColumn(name="mainId", referencedColumnName="id")},
inverseJoinColumns={#JoinColumn(name="aId", referencedColumnName="id")})
#JsonIgnore
#OrderColumn(name="order_index", columnDefinition="SMALLINT")
private List<EntityA> aList = new ArrayList<>();
...
}
#Entity
#Cache(type=CacheType.FULL)
#Table(name = "EntityA")
public class EntityA extends BaseEntity {
#Column(name = "name", columnDefinition = "VARCHAR(100)")
private String name;
#ManyToMany(mappedBy = "entityASet", fetch=FetchType.LAZY)
#JsonIgnore
private Set<MainEntity> mainEntityList = new HashSet<>();
}
#Entity
#Cache(type=CacheType.FULL)
#Table(name = "EntityB")
public class EntityB extends BaseEntity {
#Column(name = "name", columnDefinition = "VARCHAR(100)")
private String name;
#ManyToMany(mappedBy = "entityBSet", cascade=CascadeType.ALL)
#JsonIgnore
private Set<MainEntity> mainEntityList = new HashSet<>();
}
The repository query:
#QueryHints(value = {
#QueryHint(name = org.eclipse.persistence.config.QueryHints.BATCH_TYPE, value = "IN"),
#QueryHint(name = org.eclipse.persistence.config.QueryHints.BATCH_SIZE, value = "250"),
#QueryHint(name = org.eclipse.persistence.config.QueryHints.BATCH, value = "o.aList")},
#QueryHint(name = org.eclipse.persistence.config.QueryHints.BATCH, value = "o.bSet")},
forCounting = false)
List<MainEntity> findAll(Specification spec);
Generated queries:
SELECT id, STATUS, user_id FROM MainEntity WHERE ((STATUS = ?) OR ((STATUS = ?) AND (user_id = ?)))--bind => [ONESTAT, TWOSTAT, myuser]
..
SELECT t1.id, t1.name, t0.order_index FROM EntityAMapping t0, EntityA t1 WHERE ((t0.mainId = ?) AND (t1.id = t0.aId))--bind => [125e17d2-9327-4c6b-a65d-9d0bd8c040ac]
...
SELECT t1.id, t1.name, t0.mainId FROM EntityBMapping t0, EntityB t1 WHERE ((t1.id = t0.bId) AND (t0.mainId IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)))--bind => [125e17d2-9327-4c6b-a65d-9d0bd8c040ac, 1c07a3a9-7028-48ba-abe8-2296d58ebd57, 235bb4f2-d724-4237-b73b-725db2b9ca9f, 264f64b3-c355-4476-8530-11d2037b1f3c, 2d9a7044-73b3-491d-b5f1-d5b95cbb1fab, 31621c93-2b0b-4162-9e42-32705b7ba712, 39b33b19-c333-4523-a5a7-4ba0108fe9de, 40ba7706-4023-4b7e-9bd5-1641c5ed6498, 52eed760-9eaf-4f6a-a36f-076b3eae9297, 71797f0c-5528-4588-a82c-5e1d4d9c2a66, 89eda2ef-80ff-4f54-9e6a-cf69211dfa61, 930ba300-52fa-481c-a0ae-bd491e7dc631, 96dfadf9-2490-4584-b0d4-26757262266d, ae079d02-b0b5-4b85-8e6f-d3ff663afd6e, b2974160-33e8-4faf-ad06-902a8a0beb04, b86742d8-0368-4dde-8d17-231368796504, caeb79ce-2819-4295-948b-210514376f60, cafe838f-0993-4441-8b99-e012bbd4c5ee, da378482-27f9-40b7-990b-89778adc4a7e, e4d7d6b9-2b8f-40ab-95c1-33c6c98ec2ee, e557acf4-df01-4e66-9d5e-84742c99870d, ef55a83c-2f4c-47b9-99bb-6fa2f5c19a76, ef55a83c-2f4c-47b9-99bb-6fa2f5c19a77]
...
SELECT t1.id, t1.name, t0.order_index FROM EntityAMapping t0, EntityA t1 WHERE ((t0.mainId = ?) AND (t1.id = t0.aId))--bind => [1c07a3a9-7028-48ba-abe8-2296d58ebd57]
As Chris mentioned, Named Queries are the best work around for this issue. The other option is to use a custom repository and call setHint on the EntityManager yourself for each hint specified (plenty of examples out there for creating custom repos in Spring Data JPA). You could attempt to override findOne(...) and protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) on SimpleJpaRepository to try and create a generic way to properly set the hints but you'll likely want to check that you don't duplicate hint setting on getQuery(...) as you'll still want to call super() for that and then apply your additional hints before returning the query. I'm not sure what the behavior would be if you applied a duplicate hint. Save yourself the trouble and use Named Queries is my advice.

How to write complex query using criteria builder

Please help me to get the criteria builder version of following sql for the below given entity class:
sql:
select u.userid from employee e inner join user u on e.userid=u.userid
inner join employee_project ep on ep.empId=e.empId
inner join ep.projectId=p.projectId where p.projectId in (?,?...);
And also this should be able to used as a sub query too.
#Entity
#Table(name="Employee")
public class Employee {
#Id
#GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
#OneToOne(optional=false, cascade=CascadeType.ALL, orphanRemoval=true)
#JoinColumn(name="userId")
private User user;
#ManyToMany
#JoinTable(
name="Employee_Project",
joinColumns={ #JoinColumn(name="empId", referencedColumnName="empId") },
inverseJoinColumns={ #JoinColumn(name="prjId", referencedColumnName="prjId") }
)
private List<Project> project;
}

JPQL left outer join on many to many relationship

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

How to make a CriteriaBuilder join with a custom "on" condition?

I want make a query where I join 2 tables, using the CriteriaBuilder. In MySQL the query I'm trying to make would look like this:
SELECT * FROM order
LEFT JOIN item
ON order.id = item.order_id
AND item.type_id = 1
I want to get all orders and if they have an item of type #1, I want to join with this item. However, if no item of type #1 is found, I still want to get the order. I can't figure out how to make this with the CriteriaBuilder. All I know how to make is:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> cq = cb.createQuery(Order.class);
Root<Order> order = cq.from(Order.class);
Join<Order, Item> item = order.join(Order_.itemList, JoinType.LEFT);
Join<Item, Type> type = order.join(Item_.type, JoinType.LEFT);
cq.select(order);
cq.where(cb.equal(type.get(Type_.id), 1));
This query is broke, since it results in something like this in MySQL:
SELECT * FROM order
LEFT JOIN item
ON order.id = item.order_id
WHERE item.type_id = 1
The result will only contain orders with items of type #1. Orders without are excluded. How can I use the CriteriaBuilder to create a query like in the first example?
It is possible starting from the version 2.1 of JPA using the on method Join<Z, X> on(Predicate... restrictions);
Here is how:
Root<Order> order = cq.from(Order.class);
Join<Order, Item> item = order.join(Order_.itemList, JoinType.LEFT);
item.on(cb.equal(item.get(Item_.type), 1));
I think this is the same problem as posed in this question. It looks like it is not possible in CriteriaBuilder. It is possible in Hibernate Criteria API, but that probably won't help you.
JPA Criteria API: Multiple condition on LEFT JOIN
I know this question was made a long time a go, but recently a had the same problem and i found this solution from an Oracle forum, i copied and pasted just in case the link is not longer available.
MiguelChillitupaArmijos 29-abr-2011 1:41 (en respuesta a 840578) Think
you should use something like:
em.createQuery("SELECT DISTINCT e.Id" +
" from Email e " +
" left join e.idEmailIn e2 *with* e2.responseType = 'response'" +
" where e.type = 'in' and e.responseMandatory = true").getSingleResult();
An this is the link.
JPA Criteria : LEFT JOIN with an AND condition
There is a workaround if you are using Hibernate 3.6 with JPA 2.0
It is not the better solution, however it works perfect for me.
I´ve duplicate the entity with the #Where hibernate annotation.It means that everytime you use the join with this entity, hibernate will add the extra condition on the join statement at generated SQL.
For instance, initially we have the follow example:
#Entity
#Table(name = "PERSON")
public class Person {
#Id
#Column(name = "PERSON_ID")
private Long id;
#Id
#Column(name = "PERSON_NAME")
private String name;
#OneToMany(mappedBy = "person", fetch = FetchType.LAZY)
private Set<Address> addresses;
}
#Entity
#Table(name = "ADDRESS")
public class Address {
#Id
#Column(name = "ADDRESS_ID")
private Long id;
#Id
#Column(name = "ADDRESS_STREET")
private String street;
#ManyToOne
#JoinColumn(name = "PERSON_ID")
private Person person;
}
In order to add extra conditions on criteria Join, we need duplicate the Address #Entity mapping , adding the #Where annotation #Where(clause = " ADDRESS_TYPE_ID = 2").
#Entity
#Table(name = "ADDRESS")
#Where(clause = " ADDRESS_TYPE_ID = 2")
public class ShippingAddress {
#Id
#Column(name = "ADDRESS_ID")
private Long id;
#Id
#Column(name = "ADDRESS_STREET")
private String street;
#OneToOne
#JoinColumn(name = "PERSON_ID")
private Person person;
}
Also, we need to add the duplicate mapping association for the new entity.
#Entity
#Table(name = "PERSON")
public class Person {
#Id
#Column(name = "PERSON_ID")
private Long id;
#Id
#Column(name = "PERSON_NAME")
private String name;
#OneToMany(mappedBy = "person", fetch = FetchType.LAZY)
private Set<Address> addresses;
#OneToOne(mappedBy = "person")
private ShippingAddress shippingAddress;
}
Finally, you can use a join with this specific Entity in your criteria :
PersonRoot.join(Person_.shippingAddress, JoinType.LEFT);
The Hibernate Snippet SQL should seems like this :
left outer join
address shippingadd13_
on person11_.person_id=shippingadd13_.person_id
and (
shippingadd13_.ADDRESS_TYPE_ID = 2
)
ON clause is supported in Hibernate 4.3 version, anyone is aware if there is a parameter indexing issue between the parameter index of the additional custom conditions with the index of the existing mapping filters when doing an outer join with ON clause?
Using the Person entity class below as an example, say I am adding this filter to limit the address types and the filter is enabled to populate the IN clause. The parameter index for the IN clause will cause the issue [2] when I add additional conditions (such as using 'street' column) part of the ON clause. Is is a known issue?
[1] #Filter(name = "addressTypes", condition = "ADDRESS_TYPE in (:supportedTypes)")
[2]
Caused by: ERROR 22018: Invalid character string format for type BIGINT.
private Set addresses;