How to use CriteriaQuery for ElementCollection and CollectionTable - java

I have a very simple entity Product which has a code, name and tags. Tags are stored in another table (product_tag) with product_id and tag columns.
I need to search for products with certain tags using CriteriaQuery. To give an example I want to find products having 'fruit' and 'red' tags.
Using spring 4.1.x, spring-data-jpa 1.8 and hibernate 4.2.x.
My entity simply is;
#Entity
#Table(name = "product", uniqueConstraints ={
#UniqueConstraint(columnNames = "code")
}
)
#NamedQueries({
#NamedQuery(name = "Product.findAll", query = "select p from Product p")
})
public class Product extends EntityWithId {
#Column(name = "code", length = 128)
private String code;
#Column(name = "name", length = 512)
protected String name;
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name="product_tag", joinColumns=#JoinColumn(name="product_id"))
#Column(name="tag")
private Set<String> productTags = new HashSet<>();
}
here is the code how I initiate the search;
private void search() {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> criteriaQuery = builder.createQuery(Product.class);
Root<Product> product = criteriaQuery.from(Product.class);
Predicate where = builder.conjunction();
if (!StringUtils.isEmpty(nameSearch.getValue())) {
where = builder.and(where, builder.like(product.<String>get("name"), nameSearch.getValue() + "%"));
}
if (!StringUtils.isEmpty(codeSearch.getValue())) {
where = builder.and(where, builder.like(product.<String>get("code"), codeSearch.getValue() + "%"));
}
if (!StringUtils.isEmpty(tagsSearch.getValue())) {
//Util.parseCommaSeparated returns Set<String>
where = builder.and(where, product.get("productTags").in(Util.parseCommaSeparated(tagsSearch.getValue())));
}
criteriaQuery.where(where);
List<Product> resultList = entityManager.createQuery(criteriaQuery).getResultList();
}
However when I run the search for tags 'fruit' I get an exception
java.lang.IllegalArgumentException: Parameter value [fruit] did not match expected type [java.util.Set (n/a)]
I really wonder to use CriteriaQuery for ElementCollection and CollectionTable.

productTags is mapped to a separate table, therefore you need to join with that table in your query.
...
if (!StringUtils.isEmpty(tagsSearch.getValue())) {
//Util.parseCommaSeparated returns Set<String>
where = builder.and(where, product.join("productTags").in(Util.parseCommaSeparated(tagsSearch.getValue())));
}
...
Note the product.join("productTags") instead of product.get("productTags")

Try to use isMember() rather than in()
Check the example 5 and 7

Related

Hibernate/JPA many to many join tables and add as multiple strings as predicate

I'm trying to create a search where I fetch rows from Image with conditions
on columns in the Image table and keywords/tags from another table mapped via a many-to-many relationship. An image can have several tags and tags can be associated with several images.
Tables are:
**Image**
imageId
make
model
and so on....
**ImageTags**
imageId
tagId
**Tags**
tagId
tagName
Say I want to search for images with the tag "nature" the SQL would be:
SELECT DISTINCT i.* FROM Images i
INNER JOIN ImagesTags it ON it.imageId = i.imageId
INNER JOIN Tags t ON t.tagId = it.tagId
WHERE t.tagName in ("city", "nature");
There could be more and different conditions in the WHERE clause on other columns in the Image table and therefore I've tried to build dynamic queries with Criteria API since the conditions will shift on each query. But I can't get it to work.
I've succeded with only searching for keyword:
#Transactional
public List<Image> findByKeyword(String [] tags) {
List<Image> images = null;
try (Session session = entityManager.unwrap(Session.class)){
String hql = "select distinct i from Image i " +
"join i.tags t " +
"where t.tagName in (:tags)";
Query query = session.createQuery(hql);
((org.hibernate.query.Query) query).setParameterList("tags", tags);
images = query.getResultList();
} catch (Exception ex) {
ex.printStackTrace();
}
return images;
}
Now I want to add more conditions in the WHERE clause an example is:
SELECT DISTINCT i.* FROM Images i
INNER JOIN ImagesTags it ON it.imageId = i.imageId
INNER JOIN Tags t ON t.tagId = it.tagId
WHERE i.make = "Nikon" AND t.tagName in ("city", "nature");
I've tried to create a builder and add predicates but I can't seem to find a way to create the join.
CriteriaBuilder cBuilder = session.getCriteriaBuilder();
CriteriaQuery<Image> criteriaQuery = cBuilder.createQuery(Image.class);
Root<Image> imageRoot = criteriaQuery.from(Image.class);
predicateList.add(cBuilder.equal(imageRoot.get("imageId"), sc.getImageId()));
predicateList.add(cBuilder.equal(imageRoot.get("licenseType"), sc.getLicenseType()));
//more predicates are omitted
//Here somewhere I want to add an array or list of tags to the where clause and match that to the many-to-many relationship.
Predicate finalPredicate = cBuilder.and(predicateList.toArray(new Predicate[0]));
criteriaQuery.where(finalPredicate).distinct(true);
org.hibernate.query.Query<Image> imageQuery = session.createQuery(criteriaQuery);
List<Image> images = imageQuery.getResultList();
Image class:
#Entity
#Table(name = "Images")
public class Image implements File {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "imageId")
private int imageId;
#Column(name = "fileName")
private String fileName;
#Column(name = "filePath")
private String filePath;
#Column(name = "description")
private String description;
#Column(name = "fileSize")
private String fileSize;
#Column(name = "dateTime")
private LocalDateTime dateTime;
//Other attributes omitted
#ManyToMany
#JoinTable(
name = "ImagesTags",
joinColumns = #JoinColumn(name = "imageId"),
inverseJoinColumns = #JoinColumn( name = "tagId"))
private Set<Tag> tags;
//getter and setters omitted
Tag class:
#Entity
#Table(name = "Tags")
public class Tag {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "tagId")
private int tagId;
#Column(name = "tagName")
private String tagName;
#ManyToMany
#JoinTable(
name = "ImagesTags",
joinColumns = #JoinColumn(name = "tagId"),
inverseJoinColumns = #JoinColumn( name = "imageId"))
private List<Image> images;
How do I go about to create that join and add the keywords to the "WHERE tagName IN" clause?
Edit:
I've changed the code to this but it doesn't seem to work anyway
CriteriaBuilder cBuilder = session.getCriteriaBuilder();
CriteriaQuery<Image> criteriaQuery = cBuilder.createQuery(Image.class);
Root<Image> imageRoot = criteriaQuery.from(Image.class);
Root<Tag> tagRoot = criteriaQuery.from(Tag.class);
Join<Image, Tag> join = imageRoot.join(Image_.TAGS);
List<Predicate> predicateList = new ArrayList<>();
predicateList.add(cBuilder.equal(imageRoot.get("width"), 2000));
predicateList.add(cBuilder.in(join));
predicateList.add(cBuilder.equal(tagRoot.get("tagName"), tags[0]));
Predicate finalPredicate = cBuilder.and(predicateList.toArray(new Predicate[0]));
criteriaQuery.select(imageRoot);
criteriaQuery.where(finalPredicate).distinct(true);
org.hibernate.query.Query<Image> imageQuery = session.createQuery(criteriaQuery);
List<Image> images = imageQuery.getResultList();
Edit 2
Trying with this:
CriteriaBuilder cBuilder = session.getCriteriaBuilder();
CriteriaQuery<Image> criteriaQuery = cBuilder.createQuery(Image.class);
Root<Image> imageRoot = criteriaQuery.from(Image.class);
Root<Tag> tagRoot = criteriaQuery.from(Tag.class);
Join<Image, Tag> join = imageRoot.join(Image_.TAGS);
List<Predicate> predicateList = new ArrayList<>();
predicateList.add(cBuilder.in(join));
predicateList.add(cBuilder.or(cBuilder.equal(tagRoot.get("tagName"), "träd")));
Predicate finalPredicate = cBuilder.and(predicateList.toArray(new Predicate[0]));
criteriaQuery.select(imageRoot);
criteriaQuery.where(finalPredicate).distinct(true);
org.hibernate.query.Query<Image> imageQuery = session.createQuery(criteriaQuery);
List<Image> images = imageQuery.getResultList();
But it produces a (to me) very strange query to the database:
select distinct generatedAlias0
from Image as generatedAlias0,
Tag as generatedAlias1
inner join generatedAlias0.tags as generatedAlias2
where (generatedAlias2 in ()) and (
generatedAlias1.tagName=:param0
)
select distinct image0_.imageId as imageId1_1_,
image0_.author as author16_1_,
image0_.dateTime as dateTime2_1_,
image0_.description as descript3_1_,
image0_.fileName as fileName4_1_,
image0_.filePath as filePath5_1_,
image0_.fileSize as fileSize6_1_,
image0_.height as height7_1_,
image0_.licenseType as licenseT8_1_,
image0_.location as location9_1_,
image0_.make as make10_1_,
image0_.model as model11_1_,
image0_.noOfAllowedUses as noOfAll12_1_,
image0_.price as price13_1_,
image0_.resolution as resolut14_1_,
image0_.width as width15_1_
from
Images image0_
inner join
ImagesTags tags2_
on image0_.imageId=tags2_.imageId
inner join
Tags tag3_
on tags2_.tagId=tag3_.tagId cross
join
Tags tag1_
where(tag3_.tagId in ()) and tag1_.tagName=?

hibernate native query complex constructor mapping

Java, Spring Data JPA
I have 2 entities:
class Source {
Integer id;
String name;
}
class Item {
Integer id;
String name;
Integer sourceId;
}
I need statistic native query result like this:
select s.id source_id, s.name source_name, count(i.id) item_count
from source s
left join item i on s.id = i.source_id
group by s.id
And i want to have result in Java object MyResult:
class MyResult {
Source source;
Integer itemCount;
MyResult(Source source, Integer itemCount) {...}
}
The closest solution is using #SqlResultSetMapping like this:
#SqlResultSetMapping(
name = "MyResultMapping",
entities = {
#EntityResult(
entityClass = Source.class,
fields = {
#FieldResult(name = "id", column = "source_id"),
#FieldResult(name = "name", column = "source_name"),
}
),
...
???
}
)
OR
#SqlResultSetMapping(
name = "MyResultMapping",
classes = {
#ConstructorResult(
targetClass = MyResult.class,
columns = {
#ColumnResult(name = "???"),
???
}
)
}
)
With second variant i can use something like this:
MyResult(Integer sourceId, String sourceName, Integer itemsCount) {
this.source = new Source(sourceId, sourceName);
this.itemsCount = itemsCount;
}
but i want it to automate with #SqlResultSetMapping... (because my real objects more complex)
With Spring Data JPA it's better to use projections to achieve you need, for example:
public interface SourceWithItemCount {
Source getSource();
Integer getItemCount();
}
Then in your Source repository create HQL query method, like this:
public interface SourceRepo extends JpaRepository<Source, Integer> {
#Query("select s as source, count(i) like itemCount from Source s left join Item i on i.sourceId = s.id group by s"
List<SourceWithItemCount> getSourcesWithItemCount();
}
Important note is to use aliases for returned values (s as source etc.) it's allows Spring Data JPA to map them to projections properties.
Join on <condition> works from Hibernate version 5.1+ (if I'm not mistaken) so I recommend you to create classic one-to-many relation between your objects, like this, for example:
#Entity
class Source {
#Id private Integer id;
private String name;
#OneToMany #JoinColumn(name = "source_id") private List<Item> items;
}
#Entity
class Item {
#Id private Integer id;
private String name;
}
Then create JPQL query method supported by all versions of Hibernate (and other ORM providers):
#Query("select s as source, count(i) like itemCount from Source s left join s.items i group by s"
List<SourceWithItemCount> getSourcesWithItemCount();

Hibernate criteria join table issue

I have 3 entities as you can see below. I want to write a query that fetches products. In this query the parameter is a list of optionValues id.
now my question is how to join these entities?
Product:
public class Product{
//other col
#OneToMany(mappedBy = "product")
private Set<Attribute> attributeSet = new HashSet<>();
}
Attribute:
public class Attribute{
#OneToOne
#JoinColumn(name = "OPTION_VALUE_ID")
private OptionValue optionValue;
#ManyToOne
#JoinColumn(name="PRODUCT_ID",referencedColumnName="id")
private Product product;
}
optionValue:
public class OptionValue{
#Column(name = "id")
private Long id;
#Column(name = "value",updatable = true)
private String value;
}
I wrote a query but I think my code is not a good solution.
Criteria aCriteria = null;
if (!optionValueList.isEmpty()) {
aCriteria = currentSession().createCriteria(Attribute.class, "attribute");
aCriteria.createAlias("attribute.optionValue", "optionValue");
aCriteria.add(Restrictions.in("optionValue.id", optionValueList));
attributes = aCriteria.list();
}
PagingData<Product> pagingData = new PagingData<>();
Criteria criteria = currentSession().createCriteria(Product.class, "product");
if (!attributes.isEmpty()) {
for (Attribute attribute:attributes){
longList.add(attribute.getId());
}
criteria.createAlias("product.attributeSet", "attribute");
criteria.add(Restrictions.in("attribute.id", longList));
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
}
The general idea is to start with creating criteria of objects you want to return, and travel further by adding criteria which is joined. So I start with Parent class, add qualifiers and end up with most nested element, OptionValue.
Code below is untested, but you should get the idea:
Criteria criteria = currentSession()
.createCriteria(Product.class)
.createCriteria("attributeSet", "join_between_product_and_attribute");
if (!attributes.isEmpty()) {
Set<String> attributeIds = new HashSet<>();
for (Attribute attribute : attributeList) {
attributeIds.add(attribute.getId());
}
criteria.add(Restrictions.in("id", attributeIds));
}
criteria = criteria.createCriteria("optionValue", "join_between_attribute_optionvalue");
if (!optionValueList.isEmpty()) {
criteria.add(Restrictions.in("id", optionValueList));
}
an even easier solution would be to use a CriteriaQuery. i did not test the following code, but i think it should work correctly. it requires hibernate 5, but also works with some modifications in hibernate 4:
CriteriaBuilder cb = sessionFactory.getCriteriaBuilder();
CriteriaQuery<Product> query = cb.createQuery(Product.class);
Root<Product> r = query.from(Product.class);
In<Object> in = cb.in(r.join("attributeSet ").join("optionValue").get("id"));
for(Object optionValue : optionValueList){
in.value(optionValue);
}
query.select(r).where(in);
return sessionFactory.getCurrentSession().createQuery(query).getResultList();
i am assuming, that you can access the optionValueList since you posted it in your question.
For the solution with EntityManager i am assuming you already were able to instantiate one.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> query = cb.createQuery(Product.class);
Root<Product> r = query.from(Product.class);
In<Object> in = cb.in(r.join("attributeSet ").join("optionValue").get("id"));
for(Object optionValue : optionValueList){
in.value(optionValue);
}
query.select(r).where(in);
return entityManager.createQuery(query).getResultList();
if you have an EntityManagerFactory, replace the first entityManager with it and the second one with entityManagerFactory.createEntityManager()

JPA Criteria multiselect with fetch

I have following model:
#Entity
#Table(name = "SAMPLE_TABLE")
#Audited
public class SampleModel implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Long id;
#Column(name = "NAME", nullable = false)
#NotEmpty
private String name;
#Column(name = "SHORT_NAME", nullable = true)
private String shortName;
#ManyToOne(fetch = FetchType.LAZY, optional = true)
#JoinColumn(name = "MENTOR_ID")
private User mentor;
//other fields here
//omitted getters/setters
}
Now I would like to query only columns: id, name, shortName and mentor which referes to User entity (not complete entity, because it has many other properties and I would like to have best performance).
When I write query:
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<SampleModel> query = builder.createQuery(SampleModel.class);
Root<SampleModel> root = query.from(SampleModel.class);
query.select(root).distinct(true);
root.fetch(SampleModel_.mentor, JoinType.LEFT);
query.multiselect(root.get(SampleModel_.id), root.get(SampleModel_.name), root.get(SampleModel_.shortName), root.get(SampleModel_.mentor));
query.orderBy(builder.asc(root.get(SampleModel_.name)));
TypedQuery<SampleModel> allQuery = em.createQuery(query);
return allQuery.getResultList();
I have following exception:
Caused by: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=generatedAlias1,role=com.sample.SampleModel.model.SampleModel.mentor,tableName=USER_,tableAlias=user1_,origin=SampleModel SampleModel0_,columns={SampleModel0_.MENTOR_ID ,className=com.sample.credential.model.User}}]
at org.hibernate.hql.internal.ast.tree.SelectClause.initializeExplicitSelectClause(SelectClause.java:214)
at org.hibernate.hql.internal.ast.HqlSqlWalker.useSelectClause(HqlSqlWalker.java:991)
at org.hibernate.hql.internal.ast.HqlSqlWalker.processQuery(HqlSqlWalker.java:759)
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.query(HqlSqlBaseWalker.java:675)
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.selectStatement(HqlSqlBaseWalker.java:311)
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.statement(HqlSqlBaseWalker.java:259)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.analyze(QueryTranslatorImpl.java:262)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:190)
... 138 more
Query before exception:
SELECT DISTINCT NEW com.sample.SampleModel.model.SampleModel(generatedAlias0.id, generatedAlias0.name, generatedAlias0.shortName, generatedAlias0.mentor)
FROM com.sample.SampleModel.model.SampleModel AS generatedAlias0
LEFT JOIN FETCH generatedAlias0.mentor AS generatedAlias1
ORDER BY generatedAlias0.name ASC
I know that I can replace fetch with join but then I will have N+1 problem. Also I do not have back reference from User to SampleModel and I do not want to have..
I ran into this same issue, and found that I was able to work around it by using:
CriteriaQuery<Tuple> crit = builder.createTupleQuery();
instead of
CriteriaQuery<X> crit = builder.createQuery(X.class);
A little extra work has to be done to produce the end result, e.g. in your case:
return allQuery.getResultList().stream()
map(tuple -> {
return new SampleModel(tuple.get(0, ...), ...));
})
.collect(toList());
It's been a long time since the question was asked. But I wish some other guys would benefit from my solution:
The trick is to use subquery.
Let's assume you have Applicant in your Application entity (one-to-one):
#Entity
public class Application {
private long id;
private Date date;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "some_id")
private Applicant applicant;
// Other fields
public Application() {}
public Application(long id, Date date, Applicant applicant) {
// Setters
}
}
//...............
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Application> cbQuery = cb.createQuery(Application.class);
Root<Application> root = cbQuery.from(Application.class);
Subquery<Applicant> subquery = cbQuery.subquery(Applicant.class);
Root subRoot = subquery.from(Applicant.class);
subquery.select(subRoot).where(cb.equal(root.get("applicant"), subRoot));
cbQuery.multiselect(root.get("id"), root.get("date"), subquery.getSelection());
This code will generate a select statement for Application, and select statements for Applicant per each Application.
Note that you have to define an appropriate constructor corresponding to your multiselect.
I got the same problem using EclipseLink as the JPA provider : I just wanted to return the id of a mapped entity («User» in Gazeciarz's example).
This can be achieved quite simply by replacing (in the query.multiselect clause)
root.get(SampleModel_.mentor)
with something like
root.get(SampleModel_.mentor).get(User_.id)
Then, instead of returning all the fields of User, the request will only return the its id.
I also used a tuple query but, in my case, it was because my query was returning fileds from more than one entity.

Hibernate criteria implementation for this entity model (subquery, self-join)

Given the following entity one-to-many model:
One Repository can be linked to many AuditRecords.
Many AuditRecords can all link to the same Repository
#Entity
class AuditRecordEntity {
private AuditRepositoryEntity auditRepository;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = AUDIT_REPOSITORY_DB_COLUMN_NAME, nullable = false, updatable = false)
public AuditRepositoryEntity getAuditRepository() {
return auditRepository;
}
...
}
#Entity
class AuditRepositoryEntity {
private List<AuditRecordEntity> auditRecords = new ArrayList<AuditRecordEntity>();
#OneToMany(mappedBy = "auditRepository")
public List<AuditRecordEntity> getAuditRecords() {
return auditRecords;
}
...
}
Minor correction, in ERD diagram below, for 'repositoryId', read 'auditRepository'
I am trying to get the Criteria API implementation to:
Get the latest (by accessTime) AuditRecord for each distinct Repository? I.e. a list of AuditRecords, one for each Repository, where the AuditRecord is the last AuditRecord for that Repository (in the case where a Repository has many AuditRecords).
I have the HQL query to do this:
select auditRecord from AuditRecordEntity auditRecord where auditRecord.accessTime =
(select max(auditRecord2.accessTime) from AuditRecordEntity auditRecord2 where
auditRecord2.auditRepository = auditRecord.auditRepository)
But need to use the Criteria APi instead:
CriteriaBuilder builder = getEntityManager().getCriteriaBuilder();
CriteriaQuery<Object> query = builder.createQuery();
Root<AuditRecordEntity> root = query.from(AuditRecordEntity.class);
// what next?
I have got this to work(around) by using the output from the HQL query as input to the criteria API:
final List<UUID> auditRecordIds = execute("select auditRecord from AuditRecordEntity auditRecord where auditRecord.accessTime =
(select max(auditRecord2.accessTime) from AuditRecordEntity auditRecord2 where
auditRecord2.auditRepository = auditRecord.auditRepository)")
Root<AuditRecordEntity> root = criteriaQuery.from(AuditRecordEntity.class);
criteriaQuery.select(root);
List<Predicate> predicates = new ArrayList<Predicate>();
predicates.add(root.get("id").in(auditRecordIds.toArray()));
entitySearchCriteria.addPredicates(predicates);
...

Categories