Using Restrictions.disjunction over #JoinTable association - java

This is similar, but not identical, to:
Hibernate criteria query on different properties of different objects
I have a SpecChange record, which has a set of ResponsibleIndividuals; these are User records mapped by a hibernate join-table association. I want to create a Criteria query for a SpecChange which has the specified User in its ResponsibleIndividuals set, OR some other condition on the SpecChange.
I'm omitting most of the code for clarity, just showing the relevant annotations:
#Entity
class SpecChange {
#OneToMany
#JoinTable(name = "ri_id_spec_change_id", joinColumns = { #JoinColumn(name = "spec_change_id") }, inverseJoinColumns = #JoinColumn(name = "ri_id"))
#AccessType("field")
public SortedSet<User> getResponsibleIndividuals() { ... }
#Id
#Column(name = "unique_id")
#AccessType("field")
public String getId() { ... }
}
#Entity
class User { ... }
//user does not have a SpecChange association (the association is one-way)
What I want to do:
User currentUser = ...;
Criteria criteria = session.createCriteria(SpecChange.class);
...
criteria.add(Restrictions.disjunction()
.add(Restrictions.eq("responsibleIndividuals", currentUser))
.add(...)
);
criteria.list();
This generates wrong SQL:
select ... from MY_DB.dbo.spec_change this_ ... where ... (this_.unique_id=?)
...and fails:
java.sql.SQLException: Parameter #2 has not been set.
(I omitted another condition in the where clause, hence parameter #2 is the one shown. I am sure that 'currentUser' is not null.)
Note that the restriction references the wrong table: this_, which is SpecChange, not the User table
I tried a dozen different tricks to make it work correctly (including creating an alias, as mentioned in the previous post above). If there is a way to do it using the alias, I wasn't able to determine it.
The following DOES work (but doesn't accomplish what I need, since I can't use it in a disjunction):
criteria.createCriteria("responsibleIndividuals").add(Restrictions.idEq(currentUser.getId()));
[Edit: a workaround for what seems like a bug in Hibernate, using HQL]
select sc
from com.mycompany.SpecChange sc
left join fetch sc.responsibleIndividuals as scResponsibleIndividuals
where scResponsibleIndividuals = :p1 or sc.updUser = :p1
order by sc.updDate desc
This won't work without the alias "scResponsibleIndividuals"

Related

QueryDSL, Hibernate, JPA — using .fetchJoin() and getting data in first SELECT, so why N+1 queries after?

I'm trying to query for a list of entities (MyOrders) that have mappings to a few simple sub-entities: each MyOrder is associated with exactly one Store, zero or more Transactions, and at most one Tender. The generated SELECT appears correct - it retrieves all the columns from all four joined tables - but afterwards, two more SELECTs are executed for each MyOrder, one for Transactions and one for Tender.
I'm using QueryDSL 4.1.3, Spring Data 1.12, JPA 2.1, and Hibernate 5.2.
In QueryDSL, my query is:
... = new JPAQuery<MyOrder>(entityManager)
.from(qMyOrder)
.where(predicates)
.join(qMyOrder.store).fetchJoin()
.leftJoin(qMyOrder.transactions).fetchJoin()
.leftJoin(qMyOrder.tender).fetchJoin()
.orderBy(qMyOrder.orderId.asc())
.transform(GroupBy
.groupBy(qMyOrder.orderId)
.list(qMyOrder));
which is executed as:
SELECT myorder0_.ord_id AS col_0_0_,
myorder0_.ord_id AS col_1_0_,
store1_.sto_id AS sto_id1_56_1_, -- store's PK
transactions3_.trn_no AS trn_no1_61_2_, -- transaction's PK
tender4_.tender_id AS pos_trn_1_48_3_, -- tender's PK
myorder0_.ord_id AS ord_id1_39_0_,
myorder0_.app_name AS app_name3_39_0_, -- {app_name, ord_num} is unique
myorder0_.ord_num AS ord_num8_39_0_,
myorder0_.sto_id AS sto_id17_39_0_,
store1_.division_num AS div_nu2_56_1_,
store1_.store_num AS store_nu29_56_1_,
transactions3_.trn_cd AS trn_cd18_61_2_,
tx2myOrder2_.app_name AS app_name3_7_0__, -- join table
tx2myOrder2_.ord_no AS ord_no6_7_0__,
tx2myOrder2_.trn_no AS trn_no1_7_0__,
tender4_.app_name AS app_name2_48_3_,
tender4_.ord_num AS ord_num5_48_3_,
tender4_.tender_cd AS tender_cd_7_48_3_,
FROM data.MY_ORDER myorder0_
INNER JOIN data.STORE store1_ ON myorder0_.sto_id=store1_.sto_id
LEFT OUTER JOIN data.TX_to_MY_ORDER tx2myOrder2_
ON myorder0_.app_name=tx2myOrder2_.app_name
AND myorder0_.ord_num=tx2myOrder2_.ord_no
LEFT OUTER JOIN data.TRANSACTION transactions3_ ON tx2myOrder2_.trn_no=transactions3_.trn_no
LEFT OUTER JOIN data.TENDER tender4_
ON myorder0_.app_name=tender4_.app_name
AND myorder0_.ord_num=tender4_.ord_num
ORDER BY myorder0_.ord_id ASC
which is pretty much what I'd expect. (I cut out most of the data columns for brevity, but everything I need is SELECTed.)
When querying an in-memory H2 database (set up with Spring's #DataJpaTest annotation), after this query executes, a second query is made against the Tender table, but not Transaction. When querying a MS SQL database, the initial query is identical, but additional queries happen against both Tender and Transaction. Neither makes additional calls to load Store.
All the sources I've found suggest that the .fetchJoin() should be sufficient (such as Opinionated JPA with Query DSL; scroll up a few lines from the anchor) and indeed if I remove them, the initial query only selects columns from MY_ORDER. So it appears that .fetchJoin() does force generation of a query that fetches all the side tables in one go, but for some reason that extra information isn't being used. What's really weird is that I do see the Transaction data being attached in my H2 quasi-unit test without a second query (if and only if I use .fetchJoin() ) but not when using MS SQL.
I've tried annotating the entity mappings with #Fetch(FetchMode.JOIN), but the secondary queries still fire. I suspect there might be a solution involving extending CrudRepository<>, but I've had no success getting even the initial query correct there.
My primary entity mapping, using Lombok's #Data annotations, other fields trimmed out for brevity. (Store, Transaction, and Tender all have an #Id a handful of simple numeric and string field-column mappings, no #Formulas or #OneToOnes or anything else.)
#Data
#NoArgsConstructor
#Entity
#Immutable
#Table(name = "MY_ORDER", schema = "Data")
public class MyOrder implements Serializable {
#Id
#Column(name = "ORD_ID")
private Integer orderId;
#NonNull
#Column(name = "APP_NAME")
private String appName;
#NonNull
#Column(name = "ORD_NUM")
private String orderNumber;
#ManyToOne
#JoinColumn(name = "STO_ID")
private Store store;
#OneToOne
#JoinColumns({
#JoinColumn(name = "APP_NAME", referencedColumnName = "APP_NAME", insertable = false, updatable = false),
#JoinColumn(name = "ORD_NUM", referencedColumnName = "ORD_NUM", insertable = false, updatable = false)})
#org.hibernate.annotations.ForeignKey(name = "none")
private Tender tender;
#OneToMany
#JoinTable(
name = "TX_to_MY_ORDER", schema = "Data",
joinColumns = { // note X_to_MY_ORDER.ORD_NO vs. ORD_NUM
#JoinColumn(name = "APP_NAM", referencedColumnName = "APP_NAM", insertable = false, updatable = false),
#JoinColumn(name = "ORD_NO", referencedColumnName = "ORD_NUM", insertable = false, updatable = false)},
inverseJoinColumns = {#JoinColumn(name = "TRN_NO", insertable = false, updatable = false)})
#org.hibernate.annotations.ForeignKey(name = "none")
private Set<Transaction> transactions;
/**
* Because APP_NAM and ORD_NUM are not foreign keys to TX_TO_MY_ORDER (and they shouldn't be),
* Hibernate 5.x saves this toString() as the 'owner' key of the transactions collection such that
* it then appears in the transactions collection's own .toString(). Lombok's default generated
* toString() includes this.getTransactions().toString(), which causes an infinite recursive loop.
* #return a string that is unique per order
*/
#Override
public String toString() {
// use appName + orderNumber since, as they are the columns used in the join, they must (?) have
// already been set when attaching the transactions - primary key sometimes isn't set yet.
return this.appName + "\00" + this.orderNumber;
}
}
My question is: why am I getting redundant SELECTs, and how can I not do that?
I'm a little too late on the answer, but today the same problem happened to me. This response might not help you, but at least it would save someone the headache we went through.
The problem is on the relations between the entities, not in the query. I tried with QueryDSL, JPQL, and even native SQL but the problem was always the same.
The solution was to trick JPA into believing that the relations were there via annotating the child classes with #Id on those joined fields.
Basically you'll need to set Tender's id like this and use it from MyOrder like if it was a normal relationship.
public class Tender {
#EmbeddedId
private TenderId id;
}
#Embeddable
public class TenderId {
#Column(name = "APP_NAME")
private String appName;
#Column(name = "ORD_NUM")
private String orderNumber;
}
The same would go for the Transaction entity.

Criteria API is not generating left outer join

I have 3 entities, infact many more joined together for brevity i'm skipping those and i'm using open jpa 2.2.2 and oracle 11g. Any thoughts what's going wrong here?
Entity SystemRules{
#OneToMany(mappedBy = "systemRule", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private List<ServiceActionMap> serviceActionMap;
}
Entity ServiceActionMap{
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "SYSTEM_RULE_ID")
private SystemRules systemRule;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "RFS_TYPE_ID", nullable = true)
private RfsTypeMap rfsType;
}
Entity RfsTypeMap{
#Id
#Column(name="RFS_TYPE_ID" ,nullable=false)
private BigDecimal rfsTypeId;
#Column(name="RFS_NAME")
private String rfsName;
}
Now, I am trying to order the result by RfsTypeMap.rfsName, i'm creating query using criteria builder in following manner
CriteriaQuery<SystemRules> query = cb.createQuery(SystemRules.class);
Root<SystemRules> root= query.from(SystemRules.class);
root.fetch(SystemRules_.serviceActionMap).fetch(ServiceActionMap_.rfsType, JoinType.LEFT);
My order by clause is like this
cb.desc(cb.upper(systemRules.get("serviceActionMap").get("rfsType").get("rfsName").as(String.class)));
Generate JPQL looks like, where i expect a left outer join clause between ServiceActionMap and RfsTypeMap but it's missing. Same gets translated in SQL and i miss those records which are having ServiceActionMap.rfsType as null value.
SELECT DISTINCT s FROM SystemRules s INNER JOIN FETCH s.serviceActionMap INNER JOIN FETCH s.serviceActionMap INNER JOIN FETCH s.ruleProperty INNER JOIN FETCH s.ruleProperty where ... ORDER BY UPPER(s.serviceActionMap.rfsType.rfsName)
I tried going over several answers here but no success, tried explicitly putting a where clause for ServiceActionMap.rfsType is null as suggested on few answers but it's getting ignored, since join happens before where evaluation. Somewhere this question openJPA outer join on optional many-to-one when have order by clause matches my scenario but not able to generate suggested JPQL through criteria API.
I found one related bug on apache jira https://issues.apache.org/jira/browse/OPENJPA-2318. But, not sure that's the case.
I see that every join is repeated twice and even the join alias is always referring to SystemRules. It might be that orderby has caused the repeating inner joins and we may need to explicitly use Join object to refer to extended column.
CriteriaQuery<SystemRules> query = cb.createQuery(SystemRules.class);
Root<SystemRules> root = query.from(SystemRules.class);
Join<SystemRules, ServiceActionMap> join1 = root.join(SystemRules_.serviceActionMap, JoinType.INNER);
Join<ServiceActionMap, RfsTypeMap> join2 = join1.join(ServiceActionMap_.rfsType, JoinType.LEFT);
query.orderBy(cb.desc(cb.upper(join2.get(RfsTypeMap_.rfsName))));

JPA findDistinctPropertyBy magic method doesn't work as expected when using spring-boot-starter-jpa

So, here is the problem that I got.
I am building a database model structure by JPA which has a many to many relation, here is the class model
public class UserActivityRelation{
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id",nullable = false)
private User participant;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "activity_id",nullable = false)
private Activity activity;
}
here is the corresponding DAO code that I followed the spring-doc spring-boot-jpa
public List<UserActivityRelation> findDistinctParticipantByParticipantAndActivity(User user, Activity activity);
hoping to distinct rows with same user_id, but it came with an unexpected result, the sql sentence is as follows:
select distinct useractivi0_.id as id1_1_, useractivi0_.created_time
as created_2_1_, useractivi0_.modified_time as modified3_1_,
useractivi0_.activity_id as activity5_1_, useractivi0_.user_id as
user_id6_1_, useractivi0_.status as status4_1_ from act_users
useractivi0_ left outer join users user1_ on useractivi0_.user_id=user1_.id
left outer join activities activity2_ on
useractivi0_.activity_id=activity2_.id where (user1_.id is null)
and (activity2_.id is null)
the sql code seems really complex, but the point is clear, findDistinct only has effect on the primary key id rather than the participant----user_id, and it uses a lot of left outer join...
At first I thought I write the method wrong, so I test it with another method name
findDistinctHEHEByParticipantAndActivity , as you can see it has HEHE in the middle which has nothing do to with model class, neither a property nor a method... and it just gave an identical sql sentence, which I was so confused to see...
So, is it just the way that JPA with hibernate works or did I do it wrong?

Is it possible to use same table twice in a JPA criteria search

I am trying to find out if the following is possibe, using javax persistance JPA: join the same table twice, using aliases, so that two different values can be returned in the select statement. For the sample SQL below, the two values would be boss.name and employee.name, both stored in the same table in the same column.
SELECT staff.id, boss.name, employee.name
FROM staff, person as 'boss', person as 'employee',
WHERE
staff.id = boss.staff_id
AND staff.id = employee.staff_id
AND boss.code = 'boss'
AND employee.code = 'employee'
Also, I'd like to do this, if possible, with straight JDK, not Hibernate. Thanks!
I think you just need 2 relationships to Person entity from Staff, something like:
#Entity
public class Staff {
...
#OneToOne
#JoinColumn(name = "boss_id")
Person boss;
#OneToOne
#JoinColumn(name = "employee_id")
Person employee;
...
}

JPA2 Criteria: How to avoid a cross join using path.get()

If you have this entity:
#Entity
public class A {
#ManyToOne
#JoinColumn(name = "bField", nullable = true)
private B myBObject;
}
And I have a generic generator of Criteria who will do that:
Root<A> root = criteria.from(A.class);
root.get("myBObject").get("aFieldInB");
The problem is the following: the generated sql will contains a CROSS JOIN between A and B.
But I would like that the generated sql will contains a LEFT JOIN between A and B.
How can I do that?
You must use a join(). In general it is better to always use a join() for relationships.
See,
http://en.wikibooks.org/wiki/Java_Persistence/Criteria#JoinType

Categories