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

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);
...

Related

JPA Criteria query to fetch data from master and child table with entitygraph

I am trying to fetch data from parent to child both based on filter criteria using JPA Criteria query so that can avoid multiple queries to DB, but not able to achieve desired result. Following are my sample entities( without getters/setters)
#Entity
public class ParentTable implements Serializable{
private static final long serialVersionUID = 1L;
#Id
#Column(name = "ID")
private String id;
#Column(name = "KEY_COLUMN",length = 30)
private String keyColumn;
#Column(name = "CODE",length = 30)
private String code;
#Column(name = "KEY_DESC",length = 240)
private String desc;
#OneToMany(mappedBy = "parentTable",cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<ChildTable> childTableList;
}
#Entity
public class ChildTable implements Serializable{
private static final long serialVersionUID = 1L;
public ChildTable() {
super();
}
#Id
#Column(name = "ID",length = 80)
private String id;
#Column(name = "PARENT_KEY_COLUMN",length = 30,insertable = false,updatable = false)
private String parentKeyColumn;
#Column(name = "CHILD_CODE",length = 30)
private String childCode;
#Column(name = "CHILD_DESC",length = 240)
private String chldDesc;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "PARENT_KEY_COLUMN", referencedColumnName = "KEY_COLUMN")
private ParentTable parentTable;
}
Criteria builder snippet -
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<ParentTable> query = cb.createQuery(ParentTable.class);
Root<ParentTable> fromParent = query.from(ParentTable.class);
Join<ParentTable, ChildTable> details = fromParent.join("childTableList");
List<Predicate> conditions = new ArrayList();
conditions.add(cb.equal(details.get("childCode"), childCode));
conditions.add(cb.equal(details.get("chldDesc"),chldDesc));
TypedQuery<ParentTable> typedQuery = em.createQuery(query.select(fromParent).where(conditions.toArray(new Predicate[] {})));
List<ParentTable> parentTableList = typedQuery.getResultList();
This executes and gives result of parent table only, if i fetch childtable data I can see JPA query getting exceuted again, can this be avoided and fetch list of child entities which matches 3 dynamic params? 1. ParentTable.code, 2. ChildTable.childCode , 3. ChildTable.chldDesc .
Can anyone help me to construct JPA query like below which executes in one DB hit instead of multiple round trip, which is happening in above snippet of code?
select * from ParentTable p,ChildTable c where p.KEY_COLUMN=c.PARENT_KEY_COLUMN and p.CODE=? and c.CHILD_CODE=? and c.CHILD_DESC=?
Update :
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<ParentTable> cq = builder.createQuery(ParentTable.class);
Root<ParentTable> root = cq.from(ParentTable.class);
Join<ParentTable, ChildTable> join = root.join("childTableList");
Predicate p1=builder.equal(root.get("code"), "code");
Predicate p2=builder.like(join.get("chldDesc"), "%chldDesc%");
Predicate p3=builder.equal(join.get("childCode"), "childCode");
Predicate andPredicate = builder.and(p1,p2, p3);
cq.select(root).where(andPredicate);
EntityGraph<ParentTable> fetchGraph = entityManager.createEntityGraph(ParentTable.class);
fetchGraph.addSubgraph("childTableList");
List<ParentTable> parentTableList=entityManager.createQuery(cq).setHint("javax.persistence.loadgraph", fetchGraph).getResultList();
parentTableList.forEach(System.out::println);
This approach forms the expected query like above mentioned but again one more query is formed like below , why second query is triggered even-though its not required?
select * from ParentTable parentTable0_ where parentTable0_.code=?
JPA is required to give you managed entity results that reflect the data in the database. The filter you put on the query does not filter the internal relationships.
Your 'join' clause only affects the filter applied to returning ParentTable entities. Every entity returned will have a complete 'childTableList' collection, so even though you are returning ParentTable instances that have specific codes and descriptions, the childTableList shows all its children. That is JPA for you, and specific providers do have a way to filter these mapped collections (AdditionalCriteria ) IMO they are a bad route with many problems.
If you want to have childTableEntries that match the specific codes and descriptions, your query should be more of the form (using JPQL)
"Select c, p from ChildTable c join c.parentTable p where c.childCode = :code and c.chldDesc = :desc"
This will return you a List result, where the Object array has the child and parent entries for each row that matches. So duplicate parents if one has more than one child that matches.
Otherwise, the extra query is being caused by accessing the childTableList on the parentTable entries, because they are marked lazy. Your criteria query is specifying 'join', as you want to use the childTableList entries in filtering parentTable entities. If you want the childTableList fetched with the parentTables, you need to use fetchJoins. Root implements FetchParent, which would allow you to specify a 'fetch' on the childTableList in addition to the join you've defined. In JPQL, something like:
"select p from ParentTable p fetch join p.childTableList, join p.childTableList c where c.childCode = :code and c.chldDesc = :desc"

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.

How to use CriteriaQuery for ElementCollection and CollectionTable

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

JPA query a collection using between clause

My entity has a collection of another entity on which I need to do a BETWEEN criteria.
I do not want to use the native query.
I am trying to achieve this using the criteria API.
Below is a short snippet of my entity.
#Entity
#Table(name = "ref_dates")
public class Dates{
#Id
#Column(name = "ID")
private int id;
#OneToMany(fetch = FetchType.EAGER)
#JoinTable(
name="ref_dates_prg",
joinColumns = #JoinColumn( name="DATE_PRG_ID"),
inverseJoinColumns = #JoinColumn( name="DATE_ID")
)
private Set<DateInfo> dates;
}
It has several other properties, geter/setters, etc which I have not mentioned here.
I need to do a query on this Set for the id's in DateInfo object using between clause.
I tried using Expression<Set<DateInfo>> but haven't reached anywhere.
Thanks for all the help.
Here is my criteria build up.
final CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
final CriteriaQuery<NetPrgTimePeriod> criteriaQuery = criteriaBuilder.createQuery(Dates.class);
List<Predicate> criteriaList = new ArrayList<Predicate>();
final Root<Dates> root = criteriaQuery.from(Dates.class);
Join<Dates, DateInfo> dateJoin = root.join("dates", JoinType.LEFT);
Predicate runDatesRange = criteriaBuilder.between(
dateJoin.<Integer> get("id"), startDate.getId(), endDate.getId());
criteriaList.add(runDatesRange);
Join<Dates, TimeInfo> timeJoin = root.join("times", JoinType.LEFT);
Predicate timeBlocksRange = criteriaBuilder.between(
timeJoin.<Integer> get("id"), startTime.getId(), endTime.getId());
criteriaList.add(timeBlocksRange);
criteriaQuery.where(criteriaBuilder.and(criteriaList.toArray(new Predicate[0])));
TypedQuery<NetPrgTimePeriod> query = em.createQuery(criteriaQuery);
List<Dates> results = query.getResultList();
Assuming you actually mapped your collection correctly, the main part you seem to be missing is the Join:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Dates> query = cb.createQuery(Dates.class);
Root<Dates> root = query.from(Dates.class);
Join<Dates, DateInfo> infos = root.join("dates", JoinType.LEFT);
query.distinct(true);
em.createQuery(query.where(cb.between(infos.<Integer>get("id"), 1, 10))).getResultList();
Of course you can substitute metamodel fields where I used strings (which will also obsolete the need for this ugly <Integer> selector - assuming your id is an integer).

Categories