I am stuck with a problem concerning JPA-2.0 queries with relationships. How would it be possible to select any Dataset with at least one Event with type = B?
#Entity
class Dataset {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "dataset")
public List<Event> events;
}
#Entity
class Event {
#ManyToOne
#JoinColumn
public Dataset dataset;
public Type type;
}
enum Type {
A, B, C
}
My starting point is
CriteriaBuilder _builder = em.getCriteriaBuilder();
CriteriaQuery<Dataset> _criteria = _builder.createQuery(Dataset.class);
// select from
Root<Dataset> _root = _criteria.from(Dataset.class);
_criteria.select(_root);
// apply some filter as where-clause (visitor)
getFilter().apply(
_root, _criteria, _builder, em.getMetamodel()
);
// how to add a clause as defined before?
...
Any ideas on this. I tried to create a subqueries as well as a join, but I somehow did it wrong and always got all datasets as result.
Try
SELECT d FROM DataSet d WHERE EXISTS
(SELECT e FROM Event e WHERE e.dataSet = d and e.type = :type)
EDIT: As Pascal pointed out it looks like you are using the Criteria API. Not as familiar with this, but I'll have a stab.
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Dataset> criteria = builder.createQuery(Dataset.class);
Root<Dataset> root = criteria.from(Dataset.class);
criteria.select(root);
// build the subquery
SubQuery<Event> subQuery = criteria.subQuery(Event.class);
Root<Event> eventRoot = subQuery.from(Event.class);
subQuery.select(eventRoot);
ParameterExpression<String> typeParameter = builder.parameter(String.class);
Predicate typePredicate = builder.equal(eventRoot.get(Event_.type), typeParameter));
// i have not tried this before but I assume this will correlate the subquery with the parent root entity
Predicate correlatePredicate = builder.equal(eventRoot.get(Event_.dataSet), root);
subQuery.where(builder.and(typePredicate, correlatePredicate);
criteria.where(builder.exists(subQuery)));
List<DataSet> dataSets = em.createQuery(criteria).getResultList();
Phew that was hard work. I'm going back to Linq now.
Related
I have two tables A and B, A has B's foreign key, with the old Criteria API everything works fine however with the CriteriaQuery API I'm getting an error. What different is happening behind the scenes? To me it's logical that these two pieces of code should do the same thing.
Error
Column 'A.PK' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
This works:
Criteria criteria = getSession().createCriteria(A.class);
criteria.setProjection(Projections.groupProperty("b"));
return criteria.list();
this doesn't:
Session session = getSession();
CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<A> criteriaQuery = builder.createQuery(A.class);
Root<A> root = criteriaQuery.from(A.class);
criteriaQuery.groupBy(root.get("b"));
return session.createQuery(criteriaQuery).list();
Assuming that you have the following mapping:
#Entity
class A {
#ManyToOne
private B b;
}
#Entity
class B {
#Id
private Long id;
}
Outdated Criteria:
Criteria criteria = getSession().createCriteria(A.class);
criteria.setProjection(Projections.groupProperty("b"));
List<B> result = criteria.list();
actually execute the query like below:
select b_id
from A
group by b_id
and then fetch B's one by one by executing query like this:
select * from B where id = ?
When you use CriteriaBuilder hibernate actually tries to fetch data by one query. To fix the query you can do something like this:
CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<B> criteriaQuery = builder.createQuery(B.class);
Root<A> root = criteriaQuery.from(A.class);
Join<A, B> bJoin = root.join("b");
criteriaQuery.groupBy(bJoin.get("id"));
criteriaQuery.select( root.get("b") );
List<B> result = session.createQuery(criteriaQuery).list();
I have an entity Document and enum Label (not my real case, I am using analogy). The Document can have set of Labels.
The mapping of labels is following:
#Entity
public class Document {
...
#ElementCollection(fetch = FetchType.EAGER)
#Enumerated(EnumType.STRING)
#Column(name = "labels")
private Set<Label> labels = new HashSet<>();
...
}
It means labels are mapped into separated table with two columns (document_id, value) but in Java it is just enum
I need to select Documents that DO NOT have any of listed labels.
In SQL it looks like this:
select D.id
from document D left join label L
on D.id = L.document_id and L.value in('label1','label2',...)
where L.document_id is null
But I don't know how to write it in JPA Criteria API. I don't know how to express the foreign key in labels table. The JPA predicate should be something like this
CriteriaBuilder cd = ...
SetJoin<Object, Object> labelsJoin = root.joinSet("labels", JoinType.LEFT);
cb.and(labelsJoin .in("label1","label2"), cb.isNull(...???...)));
Here is my related SQL question
Thanks in advance for your suggestions.
Lukas
This should return expected result, but query statement is a bit different
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Document> query = cb.createQuery(Document.class);
Root<Document> root = query.from(Document.class);
Subquery<Long> subquery = query.subquery(Long.class);
Root<Document> subRoot = subquery.from(Document.class);
Join<Document, Label> label = subRoot.join("labels", JoinType.INNER);
List<Label> labels = Arrays.asList(Label.LABEL_1, Label.LABEL_2);
subquery.select(subRoot.get("id")).where(
cb.equal(root.get("id"), subRoot.get("id")),
label.in(labels)
);
query.select(root).where(cb.exists(subquery).not());
List<Document> result = entityManager.creteQuery(query).getResultList();
You should be easily able to translate the NOT IN option
select *
from document
where document.id not in (
select document_id
from label
where value in ('label1', 'label2')
)
into criteria API
CriteriaQuery<Document> query = cb.createQuery(Document.class);
Root<Document> root = query.from(Document.class);
query.select(root);
Subquery<Long> subquery = query.subquery(Long.class);
Root<Document> subRoot = subquery.from(Document.class);
subquery.select(subRoot.<Long>get("id"));
Join<Document, Label> labelsJoin = subRoot.join("labels");
subquery.where(labelsJoin.get("value").in("label1", "label2"));
query.where(cb.not(cb.in(root.get("id")).value(subquery)));
You might need to tweak the join between Document and Label though.
I have an entity (Person) which is a OneToOne to another entity (User). I need to find all Person entities which match User.name using CriteriaQuery.
I can do simple CriteriaQuery for direct attributes of Person just fine:
builder.like(builder.lower(root.get(column)), "%" + pattern.toLowerCase() + "%")
I'm a bit lost on how to do CriteriaQuery queries in this more complex case. From my searches here and elsewhere I think I have to use some kind of Join but I can't get my head wrapped around it.
#Entity()
public class Person extends ModelCore {
#Basic()
private String iD = null;
#OneToOne(cascade = { CascadeType.ALL })
#JoinColumns({ #JoinColumn(name = "T_User") })
private User user = null;
}
#Entity()
public class User extends ModelCore {
#Basic()
private String iD = null;
#Basic()
private String name = null;
}
#Entity()
public class ModelCore{
#Basic()
private Long dbID = null;
}
SOLVED
Nikos's solution works great (thank you!):
String username = ... // the criterion
EntityManager em = ...
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Person> query = cb.createQuery(Person.class);
Root<Person> root = query.from(Person.class);
Join<Person, User> joinUser = root.join("user");
query.where(cb.like(cb.lower(joinUser.get("name")), "%" + username.toLowerCase() + "%"));
Edit 1: Added ModelCore as base class.
Edit 2: Add working solution
Criteria API can be confusing as complexity grows. The first step I always follow is to write down the JPQL query. In this case:
SELECT p
FROM Person p
JOIN User u
WHERE LOWER(u.name) LIKE :username
Translating this to Criteria API is:
// These are initializations
String username = ... // the criterion
EntityManager em = ...
CriteriaBuilder cb = em.getCriteriaBuilder();
// Next line means "the result of the query is Person"
CriteriaQuery<Person> query = cb.createQuery(Person.class);
// This matches the "FROM Person p" part of the JPQL
Root<Person> root = query.from(Person.class);
// This matches the "JOIN User u" part of the JPQL
Join<Person, User> joinUser = root.join("user"); // if you have created the metamodel, adjust accordingly
// This is the "WHERE..." part
query.where(cb.like(cb.lower(joinUser.get("name")), "%" + username.toLowerCase() + "%"));
The WHERE part is confusing because you have to convert the infix SQL/JPQL operators to prefix (i.e. x LIKE y becomes cb.like(x, y)), but the mapping is straightforward.
I try to convert a sql query to Criteria API without success so far. I can create two separate queries which return the values I need, but I don't know how to combine them in a single query.
Here is the sql statement which works:
select company.*, ticketcount.counter from company
join
(select company, COUNT(*) as counter from ticket where state<16 group by company) ticketcount
on company.compid = ticketcount.company;
This Criteria query returns the inner query results:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<intCompany> qTicket = cb.createQuery(intCompany.class);
Root<Ticket> from = qTicket.from(Ticket.class);
Path groupBy = from.get("company");
Predicate state = cb.notEqual(from.<State>get("state"), getStateById(16));
qTicket.select(cb.construct(
intCompany.class, cb.count(from),from.<Company>get("company")))
.where(state).groupBy(groupBy);
em.createQuery(qTicket).getResultList();
In the application I defined a small wrapper/helper class:
public class intCompany{
public Company comp;
public Long opentickets;
public intCompany(Long opentickets,Company comp){
this.comp = comp;
this.opentickets = opentickets;
}
public intCompany(){
}
}
So does anyone has an idea how to get this working?
Update
Thank you. I changed my criteria query as you suggested. I just had to add a loop at the end to get the information I wanted.
List<intCompany> result = em.createQuery(cq).getResultList();
List<Company> cresult = new ArrayList();
for(intCompany ic: result){
ic.comp.setOpentickets(ic.opentickets.intValue());
cresult.add(ic.comp);
}
return cresult;
Maybe it is just not possible to convert the original sql to Criteria API.
Another update
I figured out I had to change the original sql expression to
select company.*, ticketcount.counter from company
left join
(select company, COUNT(*) as counter from ticket where state<16 group by company) ticketcount
on company.compid = ticketcount.company;
Otherwise I do not get companies with no entries in the ticket table.
So are there any other suggestions?
You have almost everything done.
//---//
CriteriaBuilder cb = em.getCriteriaBuilder();
//Your Wrapper class constructor must match with multiselect arguments
CriteriaQuery<IntCompany> cq = cb.createQuery(IntCompany.class);
//Main table
final Root<Ticket> fromTicket= cq.from(Ticket.class);
//Join defined in Ticket Entity
final Path company = fromTicket.get("company");
//Data to select
cq.multiselect(cb.count(from), company);
//Grouping
cq.groupBy(company);
//Restrictions (I don't really understand what you're querying)
Predicate p = cb.lessThan(fromTicket.get("state"), 16);
//You can add more restrictions
// p = cb.and/or(p, ...);
cq.where(p);
List<IntCompany> results = entityManager.createQuery(cq).getResultList();
This should work as expected.
I had similar problem. My solution was to use left outer joins.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Entity> query = cb.createQuery(Entity.class);
Root<Entity> root = query.from(Entity.class);
Join<Entity,ChildEntity> join = root.join(Entity_.children, JoinType.LEFT);
query.groupBy(root.get( Entity_.id ));
query.select(
cb.construct(
EntityDTO.class,
root.get( Entity_.id ),
root.get( Entity_.name ),
cb.count(join)
));
This JoinType.LEFT guarantees that you will get Entity records (companies) even if it doesn't have any child entities (tickets).
Entity class:
#Entity
public class Entity {
...
#OneToMany(targetEntity = ChildEntity.class, mappedBy = "parent", fetch = FetchType.LAZY, orphanRemoval = false)
private Set<ChildEntity> objects;
...
}
Static model:
#StaticMetamodel( Entity.class )
public class Entity_ {
public static volatile SingularAttribute<Entity, Long> id;
public static volatile SingularAttribute<Entity, String> name;
...
public static volatile SetAttribute<Entity, ChildEntity> objects;
}
I have an entity called Bucket, and I'm trying to build a criteria query to determine whether there is a Bucket stored with the "Name" property equals to "Bucket_1". So basically it is an exists query.
There is nothing special about the Bucket class, simpler impossible:
#Entity(name="Bucket")
#Table(name = "BUCKETS")
public class Bucket implements Serializable {
private static final long serialVersionUID = 1L;
#Column(name = "BUCKET_NAME", length=200)
private String Name;
...
}
For the query, this is what I go so far:
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Boolean> superQuery = criteriaBuilder.createQuery(Boolean.class);
Class<? extends T> scopeClass = Bucket.class;
Root<? extends T> root = superQuery.from(scopeClass);
Path<?> attributePath = root.get("Name");
Predicate pred = criteriaBuilder.equal(attributePath, criteriaBuilder.literal("Bucket_1"));
Subquery<? extends T> subQuery = superQuery.subquery(scopeClass);
subQuery.where(pred);
Predicate where = criteriaBuilder.exists(subQuery);
superQuery = superQuery.select(where);
/* This line fails!! */
TypedQuery<Boolean> typedQuery = em.createQuery(superQuery);
boolean result = typedQuery.getSingleResult();
When I execute the query about, I get the following exception on the last line:
Caused by: java.lang.IllegalStateException: No explicit selection and an implicit one cold not be determined
at org.hibernate.ejb.criteria.QueryStructure.locateImplicitSelection(QueryStructure.java:296)
at org.hibernate.ejb.criteria.QueryStructure.render(QueryStructure.java:249)
at org.hibernate.ejb.criteria.CriteriaSubqueryImpl.render(CriteriaSubqueryImpl.java:281)
at org.hibernate.ejb.criteria.predicate.ExistsPredicate.render(ExistsPredicate.java:57)
at org.hibernate.ejb.criteria.predicate.ExistsPredicate.renderProjection(ExistsPredicate.java:62)
at org.hibernate.ejb.criteria.QueryStructure.render(QueryStructure.java:252)
at org.hibernate.ejb.criteria.CriteriaQueryImpl.render(CriteriaQueryImpl.java:340)
at org.hibernate.ejb.criteria.CriteriaQueryCompiler.compile(CriteriaQueryCompiler.java:223)
at org.hibernate.ejb.AbstractEntityManagerImpl.createQuery(AbstractEntityManagerImpl.java:441)
at com.specktro.orchid.io.connection.database.dao.internal.DefaultDAO.has(DefaultDAO.java:426)
... 28 more
I researched a lot but couldn't find anyone with the same error having it explained and/or fixed.
I also tried this way:
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Boolean> superQuery = criteriaBuilder.createQuery(Boolean.class);
Class<? extends T> scopeClass = Bucket.class;
superQuery.from(scopeClass);
Subquery<? extends T> subQuery = superQuery.subquery(scopeClass);
Root<? extends T> root = subQuery.from(scopeClass);
Path<?> attributePath = root.get("Name");
Predicate pred = criteriaBuilder.equal(attributePath, criteriaBuilder.literal("Bucket_1"));
Subquery<? extends T> subQuery = superQuery.subquery(scopeClass);
subQuery.where(pred);
Predicate where = criteriaBuilder.exists(subQuery);
superQuery = superQuery.select(where);
/* This line fails!! */
TypedQuery<Boolean> typedQuery = em.createQuery(superQuery);
boolean result = typedQuery.getSingleResult();
But I get the same exact exception.
Does anyone know why I get this and how to fix this query?
Thank you!!
Eduardo
UPDATE:
I have been able to construct a query using exists the following way:
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Bucket> query = criteriaBuilder.createQuery(Bucket.class);
Root<Bucket> root = query.from(Bucket.class);
query.select(root);
Subquery<Bucket> subquery = query.subquery(Bucket.class);
Root<Bucket> subRootEntity = subquery.from(Bucket.class);
subquery.select(subRootEntity);
Path<?> attributePath = subRootEntity.get("Name");
Predicate pred = criteriaBuilder.equal(attributePath, criteriaBuilder.literal("Bucket_1"));
subquery.where(pred);
query.where(criteriaBuilder.exists(subquery));
TypedQuery<Bucket> typedQuery = em.createQuery(query);
boolean entityExists = typedQuery.getResultList().size() == 1;
Which resulted in a weird SQL generated like this:
select generatedAlias0 from com.test.Bucket as generatedAlias0 where exists (select generatedAlias1 from com.test.Bucket as generatedAlias1 where generatedAlias1.Name=:param0)
I guess the hard part is to get that select 1 ... going, where the result of the external query is Boolean and not the matching entity.
I know this one I got works, I'm now just trying to learn how to do it "properly", I believe there is a way...
Can you just do this?
boolean result = (session.createCriteria(Bucket.class)
.add(Restrictions.eq("Name","Bucket_1"))
.setProjection(Projections.count("Name"))
.uniqueResult() > 0);