I am unable to perform a simple fetchjoin because of MultipleBagFetchException.
#Entity
public class Person {
#OneToMany(mappedBy="person",fetch=FetchType.LAZY)
private List<Auto> autos;
}
#Entity
public class Auto {
#ManyToOne
#JoinColumn(name = "person_id", nullable = false)
private Person person;
#OneToMany(mappedBy="auto",fetch=FetchType.LAZY)
private List<Tool> tools;
}
#Entity
#Table(name="tool")
public class Tool {
#ManyToOne
#JoinColumn(name = "auto_id", nullable = false)
private Auto auto;
}
As you can see all of my associactions uses default fetchtype.
#Query("SELECT p FROM Person p JOIN FETCH p.autos a JOIN FETCH a.tools")
List<Person>findAll();
result:
Caused by: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.example.entities.Person.autos, com.example.entities.Auto.tools]
I have read about this exceptions, but in those cases the reason for this exception was the usage of EAGER fetch type for collections. What about this? This the most simple Entity relation.
And on the top of that lets suppose we are not allowed to touch the Entities.
How to solve this only on the query side?
There is one way to avoid n+1 queries without touching the entities, only changing the query for findAll. We can write a wrapper function which will first load persons with autos and them fetch all tools in a single select.
PersonRepository
#Query("SELECT distinct p FROM Person p JOIN FETCH p.autos a")
List<Person> findAll();
Wrapper code
List<Person> persons = personRepository.findAll();
Session session = (Session) entityManager.getDelegate();
List<Auto> autos = new ArrayList<>();
for (Person person : persons) {
if(!CollectionUtils.isEmpty(person.getAutos())) {
autos.addAll(person.getAutos());
}
}
try{
autos = session.createQuery("select distinct a from Auto a Join fetch a.tools " +
" where a in :autos", Auto.class)
.setParameter("autos", autos)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();
} catch (Exception ex) {
ex.printStackTrace();
}
The first query will be:
SELECT DISTINCT
person0_.id AS id1_6_0_,
autos1_.id AS id1_0_1_,
person0_.name AS name2_6_0_,
autos1_.name AS name2_0_1_,
autos1_.person_id AS person_i3_0_1_,
autos1_.person_id AS person_i3_0_0__,
autos1_.id AS id1_0_0__
FROM
Person person0_
INNER JOIN
Auto autos1_
ON
person0_.id=autos1_.person_id
The second query generated will be :
SELECT
auto0_.id AS id1_0_0_,
tools1_.id AS id1_8_1_,
auto0_.name AS name2_0_0_,
auto0_.person_id AS person_i3_0_0_,
tools1_.auto_id AS auto_id3_8_1_,
tools1_.name AS name2_8_1_,
tools1_.auto_id AS auto_id3_8_0__,
tools1_.id AS id1_8_0__
FROM
Auto auto0_
INNER JOIN
Tool tools1_
ON
auto0_.id=tools1_.auto_id
WHERE
auto0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Other than this I believe our options are limited, we will have to change Tool entity FetchMode or add BatchSize for default FetchMode.SELECT in order to get Tools in a separate query.
#OneToMany(mappedBy = "auto", fetch = FetchType.LAZY)
#Fetch(FetchMode.SUBSELECT)
private List<Tool> tools;
The query will be
SELECT
tools0_.auto_id AS auto_id3_8_1_
, tools0_.id AS id1_8_1_
, tools0_.id AS id1_8_0_
, tools0_.auto_id AS auto_id3_8_0_
, tools0_.name AS name2_8_0_
FROM
Tool tools0_
WHERE
tools0_.auto_id IN
(
SELECT
autos1_.id
FROM
Person person0_
INNER JOIN
Auto autos1_
ON
person0_.id=autos1_.person_id
)
Related
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.
I have these following Classes:
class Person(){
#OneToMany(mappedBy="person")
private List<PersonRoles> roles;
}
class PersonRoles(){
#ManyToOne
#JoinColumn(name = "person_id", nullable = false)
private Person person;
#ManyToOne
#JoinColumn(name = "request_id")
private Request request;
}
class Request(){
#OneToMany(mappedBy="request")
private List<PersonRoles> roles;
}
Now I am going to fetch all person based on a given request id and his roles by using hibernate and inner join but my log is telling me that my table doesn't exist. This is my query so far:
sql = "SELECT p.* FROM person AS p INNER JOIN p.roles ON p.roles.personId = p.id
INNER JOIN request AS r ON p.roles.requestId = r.id AND p.roles.role like :item
AND r.id = :id";
query = session.createSQLQuery(sql);
query.addEntity(Person.class);
query.setParameter("item", "Members");
query.setParameter("id", id);
person = (Person) query.uniqueResult();
and this is what i received on the log:
Table 'p.roles' doesn't exist
Did i forget some hibernate annotation? or My query has something wrong?
Brief reason
your syntax of SQL is wrong
Detailed explanation
here is the syntax of inner join example
SELECT column_name(s)
FROM table1
INNER JOIN table2
ON table1.column_name = table2.column_name;
for multiple inner join
SELECT *
FROM table1
INNER JOIN table2
ON table1.primaryKey=table2.table1Id
INNER JOIN table3
ON table1.primaryKey=table3.table1Id
but you have used INNER JOIN p.roles there should be a table name after the INNER JOIN, not a column name.
that's why you got an error, moreover, use HQL instead of SQL in hibernate it is a good practice.
happy coding!
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)
I have a NOTIFICATION table who contains one ManyToMany association :
#Entity
#Table(name="NOTIFICATION")
#NamedQuery(name="Notification.findAll", query="SELECT f FROM Notification f")
public class Notification {
/** SOME COLUMN DEFINITION NOT IMPORTANT FOR MY CASE
COD, DATE, ID_THEME, ID_TYP, IC_ARCH, ID_CLIENT, INFOS, NAME, TITRE_NOT, ID_NOT
**/
#ManyToMany
#JoinTable(
name="PJ_PAR_NOTIF"
, joinColumns={
#JoinColumn(name="ID_NOTIF")
}
, inverseJoinColumns={
#JoinColumn(name="ID_PJ_GEN")
}
)
private List<PiecesJointesGen> piecesJointesGens;
}
So, I have an association table called PJ_PAR_NOTIF.
I try to persist a Notification entity. Here is piecesJointesGens initialisation, from a Value Object :
#PersistenceContext(unitName="pu/middle")
private EntityManager entityMgr;
FoaNotification lFoaNotification = new FoaNotification();
for(PieceJointeGenVO lPJGenVO : pNotificationVO.getPiecesJointes()){
PiecesJointesGen lPiecesJointesGen = new PiecesJointesGen();
lPiecesJointesGen.setLienPjGen(lPJGenVO.getLienPieceJointeGen());
lPiecesJointesGen.setIdPjGen(lPJGenVO.getIdPieceJointeGen());
lNotification.getFoaPiecesJointesGens().add(lFoaPiecesJointesGen);
}
entityMgr.persist(pNotification);
The persist doesn't work. JPA generate a first insert for my Notification object, that is ok :
insert
into
NOTIFICATION
(COD, DATE, ID_THEME, ID_TYP, IC_ARCH, ID_CLIENT, INFOS, NAME, TITRE_NOT, ID_NOT)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Then, JPA try to insert values in my association table, but piecesJointesGen doesn't exists for the moment :
insert
into
PJ_PAR_NOTIF
(ID_NOTIF, ID_PJ_GEN)
values
(?, ?)
So, I have this error :
GRAVE: EJB Exception: : java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.entities.PiecesJointesGen
Is there a way to tell JPA to insert piecesJointesGen before the PJ_PAR_NOTIF insert ?
Modify piecesJointesGens mapping to #ManyToMany(cascade = CascadeType.PERSIST).
I am using Hibernate 4.3.8 as ORM tool for our MySql database. I have a class to be mapped which is annotated as follows:
#Entity
#DynamicUpdate
#Table(name = "myclass")
public class MyClass {
#Id
#Column(name = "myClassId")
private String id;
#Column(name = "status")
private String status;
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "myclass_children", joinColumns = #JoinColumn(name = "myClassId"))
#Column(name = "child")
#Fetch(FetchMode.JOIN)
#BatchSize(size = 100)
#Cascade(value = CascadeType.ALL)
private Set<String> children;
}
To perform read queries via Hibernate, I am asked to use Criteria API. I should mention at the beginning that using HQL or SQL are not options.
Using the following code performs exactly what I want to do: Performs a second select query to retrieve collection elements and returns exactly 20 MyClass objects.
public List<MyClass> listEntities() {
Session session = sessionFactory.openSession();
try {
Criteria criteria = session.createCriteria(MyClass.class);
criteria.setFetchMode("children", FetchMode.SELECT);
criteria.add(Restrictions.eq("status", "open"));
criteria.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
criteria.setMaxResults(20);
}
}
Here are the queries generated:
select
this.myClassId as myClassId_1_0_0,
this.status as status_2_0_0
from
myclass this
where
status = ?
limit ?
select
children0.myClassId as myClassId1_0_0,
children0.child as child2_0_0
from
myclass_children as children0_
where
children0_.myClassId in (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
However, when I try to put a restriction on collection elements, hibernate performs a single join query. When number of rows (not distinct root entities) in the result set of this single query reaches to the limit, Hibernate returns the existing MyClass objects as result. If each MyClass objects as 2 children, 10 MyClass objects are returned.
public List<MyClass> listEntities() {
Session session = sessionFactory.openSession();
try {
Criteria criteria = session.createCriteria(MyClass.class);
criteria.setFetchMode("children", FetchMode.SELECT);
criteria.createCriteria("children", "ch", JoinType.LEFT_OUTER_JOIN);
criteria.add(Restrictions.eq("status", "open"));
criteria.add(Restrictions.in("ch.elements", Arrays.asList("child1", "child2"));
criteria.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
criteria.setMaxResults(20);
}
}
Here is the generated query:
select
this.id as id1_0_0,
this.status as status2_0_0,
ch.child as child1_0_2
from
myclass this
left outer join
myclass_children ch1_
on this.myClassId = ch1_.myClassId
where
this.status = ? limit ?
What can I do to obtain 20 MyClass objects while adding restrictions on collection elements? Any suggestions & answers are welcome!
NOTE: #Fetch(FetchMode.JOIN) annotation is used for other code base (like selecting by id, etc.). It should does not have any effect on my question since I am setting FetchMode.SELECT for criteria object separately.