Hibernate many-to-many collection filtering - java

I have the following POJO with a Set inside:
class Word {
private Long id;
private String word;
private int type = WordListFactory.TYPE_DEFAULT;
private Set<Word> refs = new HashSet<Word>();
...
}
Here's the mapping XML:
<class name="kw.word.Word" table="word">
<id name="id" column="id" unsaved-value="null">
<generator class="native"/>
</id>
<property name="word"
unique="true"
not-null="true"/>
<property name="type"/>
<set name="refs"
table="word_key"
cascade="save-update">
<key column="word_id"/>
<many-to-many class="kw.word.Word"
column="word_ref_id"
fetch="join">
</many-to-many>
</set>
</class>
There are two tables: word and word_key. The latter links word-parents to word-children.
I'm trying to implement set items filtering when the data is fetched from DB. The resulting object set must contain only items with a specific type.
I tried various things:
Using filtering in mapping like (sorry for lack of brackets)
many-to-many class="kw.word.Word"
column="word_ref_id"
fetch="join">
filter name="word_type" condition="type=:type"
many-to-many
In the code that fetches data I enabled the filter and set the parameter. According to logs hibernate seems to ignore this particular filter as it there's no condition in resulting SQL query.
Using additional condition in Criteria
Word result = null;
session.beginTransaction();
Criteria crit = session.createCriteria(Word.class);
crit.add(Restrictions.like("word", key))
.createAlias("refs", "r")
.add(Restrictions.eq("r.type", getType()));//added alias and restriction for type
List list = crit.list();
if(!list.isEmpty())
result = list.get(0);
session.getTransaction().commit();
now the resulting SQL seems to be OK
select
this_.id as id0_1_,
this_.word as word0_1_,
this_.type as type0_1_,
refs3_.word_id as word1_,
r1_.id as word2_,
r1_.id as id0_0_,
r1_.word as word0_0_,
r1_.type as type0_0_
from
word this_
inner join
word_key refs3_
on this_.id=refs3_.word_id
inner join
word r1_
on refs3_.word_ref_id=r1_.id
where
this_.word like ?
and r1_.type=?
but right after this query there's another one that fetches all the items
select
refs0_.word_id as word1_1_,
refs0_.word_ref_id as word2_1_,
word1_.id as id0_0_,
word1_.word as word0_0_,
word1_.type as type0_0_
from
word_key refs0_
left outer join
word word1_
on refs0_.word_ref_id=word1_.id
where
refs0_.word_id=?
Maybe I'm doing something wrong?

From your given code snippet few points:
In case of many-to-many relationship you require 3 table , two entity tables and one join table. But as you are having same entity -Word , i think the given table structure and mappings seems fine.
Try to use HQL and specify 'LEFT JOIN FETCH' to specify which associations you need to be retrieved in the initial sql SELECT.
See this link related to many-to-many relationship,but they used criteria query.
Querying ManyToMany relationship with Hibernate Criteria

Related

org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags

Following is my code Here I am using multiple lists to fetch data from database.
On fetching data from hql query it is showing exception.
Pojo Class
public class BillDetails implements java.io.Serializable {
private Long billNo;
// other fields
#LazyCollection(LazyCollectionOption.FALSE)
private List<BillPaidDetails> billPaidDetailses = new ArrayList<BillPaidDetails>();
private Set productReplacements = new HashSet(0);
#LazyCollection(LazyCollectionOption.FALSE)
private List<BillProduct> billProductList = new ArrayList<BillProduct>();
//getter and setter
}
hmb.xml file
<class name="iland.hbm.BillDetails" table="bill_details" catalog="retail_shop">
<id name="billNo" type="java.lang.Long">
<column name="bill_no" />
<generator class="identity" />
</id>
<bag name="billProductList" table="bill_product" inverse="true" lazy="false" fetch="join">
<key>
<column name="bill_no" not-null="true" />
</key>
<one-to-many class="iland.hbm.BillProduct" />
</bag>
<bag name="billPaidDetailses" table="bill_paid_details" inverse="true" lazy="false" fetch="select">
<key>
<column name="bill_no" not-null="true" />
</key>
<one-to-many class="iland.hbm.BillPaidDetails" />
</bag>
<set name="productReplacements" table="product_replacement" inverse="true" lazy="false" fetch="join">
<key>
<column name="bill_no" not-null="true" />
</key>
<one-to-many class="iland.hbm.ProductReplacement" />
</set>
</class>
Hql query
String hql = "select distinct bd,sum(bpds.amount) from BillDetails as bd "
+ "left join fetch bd.customerDetails as cd "
+ "left join fetch bd.billProductList as bpd "
+ "left join fetch bpd.product as pd "
+"left join fetch bd.billPaidDetailses as bpds "
+ "where bd.billNo=:id "
+ "and bd.client.id=:cid ";
I am trying following query to fetch data from database but this is showing
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags
How to resolve this
Hibernate doesn't allow fetching more than one bag because that would generate a Cartesian product.
Now, you will find lots of answers, blog posts, videos, or other resources telling you to use a Set instead of a List for your collections.
That's terrible advice!
Using Sets instead of Lists will make the MultipleBagFetchException go away, but the Cartesian Product will still be there.
The right fix
Instead of using multiple JOIN FETCH in a single JPQL or Criteria API query:
List<Post> posts = entityManager.createQuery("""
select p
from Post p
left join fetch p.comments
left join fetch p.tags
where p.id between :minId and :maxId
""", Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.getResultList();
You can do the following trick:
List<Post> posts = entityManager.createQuery("""
select distinct p
from Post p
left join fetch p.comments
where p.id between :minId and :maxId
""", Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();
posts = entityManager.createQuery("""
select distinct p
from Post p
left join fetch p.tags t
where p in :posts
""", Post.class)
.setParameter("posts", posts)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();
As long as you fetch at most one collection using JOIN FETCH, you will be fine. By using multiple queries, you will avoid the Cartesian Product since any other collection but the first one is fetched using a secondary query.
For me I had the same error and I solved by adding the annotation of hibernate
#Fetch
#OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
#Fetch(value = FetchMode.SUBSELECT)
private List<Child> childs;
Changing to Set is the best solution. However, if you cannot not replace the List with Set (like in my case, there was a heavy use of JSF tags specific to Lists), and if you can use Hibernate proprietary annotations, you can specify #IndexColumn (name = "INDEX_COL"). That solution worked better for me, changing to Set would require tons of refactoring.
So, your code would be something like this:
#IndexColumn (name = "INDEX_COL")
private List<BillPaidDetails> billPaidDetailses = new ArrayList<BillPaidDetails>();
#IndexColumn (name = "INDEX_COL")
private List<BillProduct> billProductList = new ArrayList<BillProduct>();
As Igor suggested in the comments, you could also create proxy methods to return the lists. I haven't tried that, but would be a good alternative if you cannot use Hibernate proprietary annotations.
You can only join-fetch following one relation for an entity (either billPaidDetailses or billProductList).
Consider using lazy associations and loading collections when they are needed, OR using lazy associations and loading collections manually with Hibernate.initialize(..). At least that was the conclusion I came to when I had a similar issue.
Either way it will take more than one query to the database.
I find using #PostLoad annotated method in the entity most useful, I'd do something like
#PostLoad
public void loadCollections(){
int s1 = productReplacements.size();
int s2 = billProductList.size();
}
this way I'm able to fine control the eager loading and initialization of collections in the same transaction that loaded the entity.
I used the new annotation #OrderColumn instead of #IndexColumn (deprecated see: https://docs.jboss.org/hibernate/orm/5.2/javadocs/org/hibernate/annotations/IndexColumn.html) and it works now.
Annotate one of the collections with #OrderColumn
e.g.
#ManyToMany(cascade = CascadeType.ALL)
#OrderColumn
private List<AddressEntity> addresses = Lists.newArrayList();
#Builder.Default
#ManyToMany(cascade = CascadeType.ALL)
private List<BankAccountEntity> bankAccounts = Lists.newArrayList();
Your request fetch too many data and HIbernate cannot load them all.
Reduce your request and/or configure your entities to retrieve just needed data

Why many-to-one bidirectional association requires update and insert set to false

The Hibernate document says that if I want to use a list then I need to set the properties for update="false" and insert="false".
Please let me know why these attributes are needed and how is this useful?
If you use a List, or other indexed collection, set the key column of
the foreign key to not null. Hibernate will manage the association
from the collections side to maintain the index of each element,
making the other side virtually inverse by setting update="false" and
insert="false":
<class name="Person">
<id name="id"/>
...
<many-to-one name="address"
column="addressId"
not-null="true"
insert="false"
update="false"/>
</class>
<class name="Address">
<id name="id"/>
...
<list name="people">
<key column="addressId" not-null="true"/>
<list-index column="peopleIdx"/>
<one-to-many class="Person"/>
</list>
</class>
I have also gone through this post Setting update and insert property in Hibernate, but when I wrote a simple program to create and save my Person and Address objects I can see that addressId property is inserted and updated by hibernate itself:
Hibernate: insert into Address (addressId) values (?)
Hibernate: insert into person1 (addressId, peopleId, personId) values (?, ?, ?)
Hibernate: insert into person1 (addressId, peopleId, personId) values (?, ?, ?)
09:19:08,526 DEBUG AbstractCollectionPersister:1205 - Inserting collection: [partc.onetomany1.Address.people#156]
Hibernate: update person1 set addressId=?, peopleId=? where personId=?
Hibernate: update person1 set addressId=?, peopleId=? where personId=?
But as per comments given by JB Nizet and Thomas, this should not happen. Please let me know if I misunderstood the concept.
Consider the example from the Hibernate Documentation. And i can try to help as much as i can.
#Entity
public class Troop {
#OneToMany(mappedBy="troop")
public Set<Soldier> getSoldiers() {
...
}
#Entity
public class Soldier {
#ManyToOne
#JoinColumn(name="troop_id")
public Troop getTroop() {
...
}
Your table structure in this case would be
**Troop**
Troop_id | Troop_desc
T1 | Troop 1
T2 | Troop 2
**Soldier**
Soldier_Id | Troop_Id | Soldier_Name
S1 | T1 | XYZ
S2 | T2 | PQR
**Consider in the above situation you need to add a Soldier the function calls would be
1) create the Soldier with the Troop and save it on the Soldier table.**
**Consider this to be managed from the Troop entity, where you would have to do the following transactions
1) you append the soldier to the soldiers collection in the Troop entity
2) create the soldier.
3) update the Troop entity too.**
So, that is why you would have an insert and an update in the second condition where you have a #OneToMany as the managing side.
Now, coming back to why the documentation mentions that the #ManytoOne JoinColumn should be insertable=false, updateable = false is because in the second scenario, where (#OneToMany side of the relation is the managing side) you would update the Soldier by updating the Troop entity.
I think it ties back to how you design your application on what should be the relation which you want to manage and from where.

how do I do the following hibernate mapping?

I have the following DB schema :
table a {
id,
state
}
table b {
id,
a_id,
is_valid,
amount
}
I want to have a hibernate mapping where I fetch values from table b only if a.state has a certain value. This is the hibernate mapping i had (used the example from the jBoss Documentation)
<discriminator column="state" type="string"/>
<subclass name="ClassB" discriminator-value="VALUE1">
<join table="b">
<key column="a_id"/>
<property name="amount" column="amount"/>
</join>
</subclass>
When i did this, my xml showed a syntax error stating that a hierarchy must be followed.
Is what I'm doing correct and if not, it would be great if someone could show me the way forward. Thanks.
P.S - more than one entry in table b will have the a_id column. However only one row in b will have the is_valid value set and its enough if i get this row in my POJO
It looks to me like you are mapping a table per subclass with discriminator strategy. This would imply a 1 - 1 row correlation between table a and table b, where the primary key of table b (the subclass) would also be a foreign key into table a.
However, your mapping is slightly odd in that you have
<key column="a_id" />
Typically this should be
<key column="id" />
And there would be no "a_id" column.
However, your db design looks like a one-to-many relationship rather than a subclass relationship.
Without your objects themselves, i can't really say what it is you're trying to do.
Take a look at the hibernate docs on inheritence.
http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html/inheritance.html

Why is hibernate deleting rows from join table when adding element to set mapping many-to-many?

Suposse I have two classes:
class A {
Set<B> bs
}
class B {
}
This mapping:
<set name="bs" table="bs_tab" cascade = "save-update">
<key column="a_id />
<many-to-many column="b_id" class="B"/>
</set>
And join table like this:
bs_tab(
a_id, b_id, primary key(a_id, b_id)
)
When I add some element to bs set and then call Session.saveOrUpdate(A instance) hibernate is deleting all rows in bs_tab coresponding to B instances that were in the set before adding new element.
How can I solve this?
Make sure to implement equals/hashCode correctly. I have the same kind of mapping (unidirectional many-to-many) and adding elements does not generate DELETE then INSERT SQL statements for the join table.

Update one value from a list of dependent objects

Given an entity with a list of components:
class Entity{
Long id;
String name;
List<Component> components = new ArrayList<Component>();
}
class Component{ Object value; }
Configuration:
<hibernate-mapping>
<class name="Entity" table="entity">
<id name="id" access="field" column="id"/>
<property name="name" access="field" unique="true"/>
<list name="components" access="field" table="COMPONENTS" lazy="true">
<key column="id"/>
<list-index column="idx"/>
<composite-element class="Component">
<property name="value" access="field"/>
</composite-element>
</list>
</class>
</hibernate-mapping>
Is it possible to update one component from the list with HQL statement like
update Entity e set e.components[:index].value = :value where e.name = :name
that does not work?
Alternatively, is it possible to configure lazy loading of the list of components in a way that the first access:
entity.components.get(0).value = "..";
does not load the whole list?
Edit:
The lazy="extra" configuration does work for select (loads only the component to update), but it will not update the changed component.
You can't update a single collection element via HQL.
From the 13.4. DML-style operations chapter:
There can only be a single entity named in the from-clause.
No joins, either implicit or explicit, can be specified in a bulk HQL query.
Since your collection element is not an entity, it's not addressable from within bulk update. Technically speaking, non-entity collection elements are not addressable in general; indexed collections or sets with elements having natural ids being the only exceptions.
While it is possible to lazy-load collection elements few at a time (though it doesn't really make sense in this case unless you know ahead of time that you'll only be looking at Nth element since batch size is not easily changeable at runtime), it's not going to help because entire collection will be loaded anyway when you try to update it.
Selecting a single collection element is possible for indexed collection (not part of your question, but I wanted to clarify on this based on KLE answer and your comments):
select c
from Entity e join e.components c
where index(c) = :index

Categories

Resources