JPA CriteriaBuilder left outer join for table with no relations? - java

Initially I had requirement to write code using JPA CriteraiBuilder for following SQL:
SELECT ve.col_1,
(SELECT vm.col_4
FROM table2 vm
WHERE vm.col_2 = ve.col_2
AND vm.col_3 = ve.col_3
) as col_a
FROM table1 ve;
But I learnt that, it is not possible to add subquery in select clause. So I changed my query to use left outer join like this.
SELECT ve.col_1,
vm.col_4 as col_a
FROM table1 ve,
table2 vm
WHERE
vm.col_2 (+) = ve.col_2
AND vm.col_3 (+) = ve.col_3;
Now table1 and table2 do not have direct relations using foreign keys. Corresponding JPA entities look like:
Table1.java ->
#Column(name = "COL_1")
private String col_1;
#Column(name = "COL_2")
private String col_2;
#Column(name = "COL_3")
private String col_3;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumns({
#JoinColumn(name="COL_2"),
#JoinColumn(name="COL_3")
})
private Table2 table2;
Table2.java ->
#Column(name = "COL_4")
private String col_4;
#Column(name = "COL_2")
private String col_2;
#Column(name = "COL_3")
private String col_3;
My code looks like:
final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
final CriteriaQuery<SearchTO> query = criteriaBuilder.createQuery(
SearchTO.class);
Root<Table1> root = query.from(Table1.class);
final Join<Table1, Table2> joinTable2 = root.join(Table1_.table2,
JoinType.LEFT);
Then I am trying to fetch value using:
joinTable2.get(Table2_.col_4)
Then now I am getting error as:
A Foreign key refering com.Table2 from com.Table1 has the wrong number of column
Table2 has around 6 columns with annotation #Id and I can not change change it to have only two columns with #Id annotation.
Please let me know:
If it is possible to write code using CriteriaBuilder for my approach 1 (subquery in select clause).
If thats not possible, how can I implement this left outer join as mentioned for approach 2. Please note that Table2 does not have any references of Table1.
Please note that I am using plain JPA APIs. DB is Oracle11g. JDK version is 1.7.

One solution is to create view and query against it using criteria builder.
You can see my answer in Joining tables without relation using JPA criteria
Only change you need to do is use left join in your view definition.
Hope it solves your use case.

For Approach 1: You can write subquery for criteria query
Subquery<Entity1> subquery = cq.subquery( Entity1.class );
Root fromSubQuery = subquery.from( Entity1.class );
subquery.select( cb.max( fromSubQuery.get( "startDate" ) ) );
subquery.where( cb.equal( fromSubQuery.get( "xyzId" ), fromRootOfParentQuery.get( "xyzId" ) ) );
Use it as :
Root<Entity2> entity2 = cq.from( Entity2.class );
Predicate maxDatePredicate = cb.and( cb.equal( entyty2.get( "startDate" ), subquery ) );
For Approach 2:
There is no other way than having relationship between two entities for left join. You can define private variable for relationship without getter & setter and use that variable for setting left join.
Then add the predicate to criteriaBuilder

Related

JPA criteria: order by child field gives error

I have 3 entities. Customer, Process and Document.
A Customer has many processes and a process has many documents.
I want to sort customers by document's updateDate.
My entities are like below;
Customer-
#Entity
public class Customer {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Process> processes = new ArrayList<>();
// getter, setter etc.
}
Process-
#Entity
public class Process {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String type;
#ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
#OneToMany(mappedBy = "process", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Document> documents = new ArrayList<>();
//getter, setter etc.
}
Document-
#Entity
public class Document {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String note;
private LocalDateTime updateDate;
#ManyToOne(fetch = FetchType.LAZY)
private Process process;
}
I have tried the following specification-
public static Specification<Customer> orderByDocumentUploadDate() {
return (root, query, criteriaBuilder) -> {
ListJoin<Customer, Process> processJoin = root.join(Customer_.processes);
ListJoin<Process, Document> documentJoin = processJoin.join(Process_.documents);
query.orderBy(criteriaBuilder.desc(documentJoin.get(Document_.updateDate)));
query.distinct(true);
return null;
};
}
It gives following error-
ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select
list
Generated SQL-
select distinct customer0_.id as id1_0_,
customer0_.name as name2_0_
from customer customer0_
inner join
process processes1_ on customer0_.id = processes1_.customer_id
inner join
document documents2_ on processes1_.id = documents2_.process_id
order by documents2_.update_date desc
limit ?
I have also tried by grouping, like below-
public static Specification<Customer> orderByDocumentUploadDate() {
return (root, query, criteriaBuilder) -> {
ListJoin<Customer, Process> processJoin = root.join(Customer_.processes);
ListJoin<Process, Document> documentJoin = processJoin.join(Process_.documents);
query.orderBy(criteriaBuilder.desc(documentJoin.get(Document_.updateDate)));
query.groupBy(root.get(Customer_.id));
return null;
};
}
Then it gave a different error-
ERROR: column "documents2_.update_date" must appear in the GROUP BY
clause or be used in an aggregate function
Generated SQL-
select
customer0_.id as id1_0_,
customer0_.name as name2_0_
from
customer customer0_
inner join
process processes1_
on customer0_.id=processes1_.customer_id
inner join
document documents2_
on processes1_.id=documents2_.process_id
group by
customer0_.id
order by
documents2_.update_date desc limit ?
I could do it by the following sql; max() solved it in below sql-
select customer.* from customer
inner join process p on customer.id = p.customer_id
inner join document d on p.id = d.process_id
group by customer.id
order by max(d.update_date);
But I can't do the same, using the criteria API.
Do you have any suggestion?
This is a conceptual misunderstanding.
First, you have to understand how does inner join works. And this portion is okay in this case: [join process table with document table based on document.process_id = process.id]
Second, you need to sort customers based on the document's update date
Unfortunately, you used group by here. GROUP BY only returns column in which it is grouped by. In this case, it will return only customer_id.
You can use aggregate functions like count(), sum() etc. on grouped data.
When you tried to access update_date, it will throw below error:
ERROR: column "documents2_.update_date" must appear in the GROUP BY clause or be used in an aggregate function
Now, how can we get rid of this?
So first we need to do join to get customer id. After getting customer id, we should group the data by the customer id and then use max() to get max_date of each group(if necessary then minimum)
SELECT
customer_id,
max(date) AS max_date
FROM
document
JOIN process ON process.id = document.process_id
GROUP BY customer_id
It will return a temporary table, that looks something like below:
customer_id
max_date
1
2020-10-24
2
2021-03-15
3
2020-09-24
4
2020-03-15
Using the temporary table, you can now sort customer_id by date
SELECT
customer_id,
max_date
FROM
(SELECT
customer_id,
max(date) AS max_date
FROM
document
JOIN process ON process.id = document.process_id
GROUP BY customer_id) AS pd
ORDER BY max_date DESC
Hope this helps.

JPA Criteria API - joining ElementCollection with WHERE condition on ElementCollection foreign key

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.

Hibernate failing to parse native query containing STRAIGHT_JOIN keyword

I have a native sql query used inside #Formula annotation in a hibernate entity, but Hibernate is failing to parse it, and throws a syntax error exception.
The entity/query is something similar to the following:
#Table(name="table2")
public class Table2{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Formula("(SELECT MAX(table1.col1) from table1 STRAIGHT_JOIN table2 t ON"
+ "(table1.col2 = t.col2) INNER JOIN table3 AS t3 ON (t3.col1 = t.col1)
WHERE t3.col2 = 1)")
private Integer code;
#Column
private Type type;
....
//getters and setters
....
}
On looking at the mysql query logs, following is the actual query being executed:
select table2x0_.id, table2x0_.type, (SELECT MAX(table1.col1) from table1
STRAIGHT_JOIN table2x0_.table2 table2x0_.t ON (table1.col2 = t.col2) INNER JOIN
table3 AS t3 ON (t3.col1 = t.col1) WHERE t3.col2 = 1) as formula1_ from table2
table2x0_ where table2x0_.type in (0,1,2);
It appears that hibernate is having issues with the STRAIGHT_JOIN keyword. Instead of STRAIGHT_JOIN table2 t, the query is being written as STRAIGHT_JOIN table2x0_.table2 table2x0_.t, i.e., it is prefixing the outer query's table alias.
If I replace STRAIGHT_JOIN with INNER JOIN, the query works fine. But I need to use STRAIGHT_JOIN for my use case.
Can someone please help with how to resolve it?

eclipselink AdditionalCriteria ignored in child class

If I setup a parent/child relationship with both parent and child having additionalcriteria constraints, and then use #JoinFetch then childs additionalcriteria are ignored.
For example:
TableA.java:
#javax.persistence.Entity
#Table(name = "TABLE_A")
#AdditionalCriteria("this.tableAfield2=:propA")
public class TableA {
#Id
#Column(name = "TABLEAFIELD1")
private String tableAfield1;
#Column(name = "TABLEAFIELD2")
private String tableAfield2;
#JoinColumn(name = "TABLEAFIELD2", referencedColumnName = "TABLEBFIELD1", insertable = false, updatable = false)
#OneToOne(fetch = FetchType.EAGER)
// #JoinFetch(JoinFetchType.OUTER)
private TableB tableAtableB;
}
TableB.java:
#javax.persistence.Entity
#Table(name = "TABLE_B")
#AdditionalCriteria("this.tableBfield2=:propB")
public class TableB {
#Id
#Column(name = "TABLEBFIELD1")
private String tableBfield1;
#Column(name = "TABLEBFIELD2")
private String tableBfield2;
public String getTableBfield1() {
return tableBfield1;
}
public String getTableBfield2() {
return tableBfield2;
}
}
Main:
em.setProperty("propA", "propertyAValue");
em.setProperty("propB", "propertyBValue");
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<TableA> criteriaQuery = cb.createQuery(TableA.class);
Root<TableA> tableA = criteriaQuery.from(TableA.class);
Predicate pred = cb.equal(tableA.get("tableAfield1"), "keyA1");
criteriaQuery.where(pred);
List<TableA> results = em.createQuery(criteriaQuery).getResultList();
With tableA set as per the example (with JoinFetch commented out)
the applications creates 2 SQLs
SELECT TABLEAFIELD1, TABLEAFIELD2 FROM TABLE_A WHERE ((TABLEAFIELD1 = ?) AND (TABLEAFIELD2 = ?))
bind => [keyA1, propertyAValue]
SELECT TABLEBFIELD1, TABLEBFIELD2 FROM TABLE_B WHERE ((TABLEBFIELD1 = ?) AND (TABLEBFIELD2 = ?))
bind => [propertyAValue, propertyBValue]
which is fine, as eclipselink is loading the table_b on demand.
but for our application we need to have a single SQL, as there maybe 1000s of rows and we need a single join.
So, if I put back the #JoinFetch then the sql generated is;
SELECT t1.TABLEAFIELD1, t1.TABLEAFIELD2, t0.TABLEBFIELD1, t0.TABLEBFIELD2 FROM TABLE_A t1 LEFT OUTER JOIN TABLE_B t0 ON (t0.TABLEBFIELD1 = t1.TABLEAFIELD2) WHERE ((t1.TABLEAFIELD1 = ?) AND (t1.TABLEAFIELD2 = ?))
bind => [keyA1, propertyAValue]
the additionalCriteria from TableB is not added (there is no t0.tableBField1=? (propertyBValue) )
Any suggestions? Its driving me mad.
Many thanks
For completeness here are the tables
create table TABLE_A (
TABLEAFIELD1 varchar2(20),
TABLEAFIELD2 varchar2(30),
CONSTRAINT tableApk PRIMARY KEY (TABLEAFIELD1)
) ;
create table TABLE_B (
TABLEBFIELD1 varchar2(20),
TABLEBFIELD2 varchar2(30),
CONSTRAINT tableBpk PRIMARY KEY (TABLEBFIELD1)
) ;
insert into TABLE_A (TABLEAFIELD1,TABLEAFIELD2) values ('keyA1','propertyAValue');
insert into TABLE_A (TABLEAFIELD1,TABLEAFIELD2) values ('keyA2','propertyAValue');
insert into TABLE_A (TABLEAFIELD1,TABLEAFIELD2) values ('keyA3','random');
insert into TABLE_B (TABLEBFIELD1,TABLEBFIELD2) values ('propertyAValue','propertyBValue');
So this is a long term bug with eclipselink and doesn't look like it will be fixed.
The solution was to change
#JoinFetch(JoinFetchType.OUTER)
to
#BatchFetch(BatchFetchType.JOIN)
This doesn't exactly have the result I was hoping for, originally wanted the generated sql to include an OUTER JOIN,
but BatchFetch results in only 2 SQLs, one to get the Table_A items, then another to fetch all the Table_B items (including the additionalcriteria requirements)

Hibernate Criteria Api Subqueries

I am currently working on a project to transfer some legacy jdbc select statements over to using Hibernate and it's criteria api.
The two relevant table columns and the SQL query looks like:
-QUERIES-
primaryId
-QUERYDETAILS-
primaryId
linkedQueryId -> Foreign key references queries.primaryId
value1
value2
select *
from queries q
where q.primaryId not in (SELECT qd.linkedQueryId
FROM querydetails qd
WHERE (qd.value1 LIKE 'PROMPT%'
OR qd.value2 LIKE 'PROMPT%'));
My entity relationships look like:
#Table("queries")
public class QueryEntity{
#Id
#Column
private Long primaryId;
#OneToMany(targetEntity = QueryDetailEntity.class, mappedBy = "query", fetch = FetchType.EAGER)
private Set<QueryDetailEntities> queryDetails;
//..getters/setters..
}
#Entity
#Table(name = "queryDetails")
public class QueryDetailEntity {
#Id
#Column
private Long primaryId;
#ManyToOne(targetEntity = QueryEntity.class)
private QueryEntity query;
#Column(name="value1")
private String value1;
#Column(name="value2")
private String value2;
//..getters/setters..
}
I am attempting to utilize the criteria api in this way:
Criteria crit = sessionFactory.getCurrentSession().createCriteria(QueryEntity.class);
DetachedCriteria subQuery = DetachedCriteria.forClass(QueryDetailEntity.class);
LogicalExpression hasPrompt = Restrictions.or(Restrictions.ilike("value1", "PROMPT%"),
Restrictions.ilike("value2", "PROMPT%"));
subQuery.add(hasPrompt);
Criterion subQueryCrit = Subqueries.notIn("queryDetails", subQuery);
crit.add(subQueryCrit);
List<QueryMainEntity> entities = (List<QueryMainEntity>) crit.list();
System.out.println("# of results = " + entities.size());
I am getting a NullPointerException on the crit.list() line that looks like
Exception in thread "main" java.lang.NullPointerException
at org.hibernate.loader.criteria.CriteriaQueryTranslator.getProjectedTypes(CriteriaQueryTranslator.java:362)
at org.hibernate.criterion.SubqueryExpression.createAndSetInnerQuery(SubqueryExpression.java:153)
at org.hibernate.criterion.SubqueryExpression.toSqlString(SubqueryExpression.java:69)
at org.hibernate.loader.criteria.CriteriaQueryTranslator.getWhereCondition(CriteriaQueryTranslator.java:380)
at org.hibernate.loader.criteria.CriteriaJoinWalker.<init>(CriteriaJoinWalker.java:114)
at org.hibernate.loader.criteria.CriteriaJoinWalker.<init>(CriteriaJoinWalker.java:83)
at org.hibernate.loader.criteria.CriteriaLoader.<init>(CriteriaLoader.java:92)
at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1687)
at org.hibernate.impl.CriteriaImpl.list(CriteriaImpl.java:347)
Now, I think its pretty safe to say I'm using the Criteria Api/Detached Query Api incorrectly, but I'm not sure what the 'correct' way to do it is since the Hibernate Docs only briefly cover criteria api subqueries.
I realize this is a pretty long question, but I figure its appear to put it all the relevant aspects of the question (query I'm attempting to represent via Criteria API, tables, entities).
Give this a shot:
DetachedCriteria d = DetachedCriteria.forClass(QueryDetailEntity.class, "qd");
d.setProjection(Projections.projectionList().add(Projections.property("qd.query")));
d.add(Restrictions.or(Restrictions.like("qd.value1", "PROMPT%"), Restrictions.like("qd.value2", "PROMPT%")));
criteria = session.createCriteria(QueryEntity.class, "q");
criteria.add(Subqueries.propertyNotIn("q.primaryId", d));
criteria.list();
The use of the following are property names, not column names:
qd.query
qd.value1
qd.value2
q.primaryId
As a side note, if this is not a dynamically generated query, have you given thought to using HQL instead?

Categories

Resources