I have two entity beans defined as follows (unrelated stuff removed):
@Entity
@Table(...)
public class MasterItem implements java.io.Serializable {
private Set<CriticalItems> criticalItemses = new HashSet<CriticalItems>(0);
@OneToMany(fetch = FetchType.EAGER, mappedBy = "masterItem", orphanRemoval = true,
cascade = {javax.persistence.CascadeType.DETACH})
@Cascade({CascadeType.SAVE_UPDATE, CascadeType.DELETE})
public Set<CriticalItems> getCriticalItemses() {
return this.criticalItemses;
}
}
CriticalItems is defined as follows:
@Entity
@Table(...)
public class CriticalItems implements java.io.Serializable {
private MasterItem masterItem;
@ManyToOne(fetch = FetchType.LAZY, optional = false,
cascade = {javax.persistence.CascadeType.DETACH})
@Cascade({CascadeType.SAVE_UPDATE})
@JoinColumn(name = "mi_item_id", nullable = false)
public MasterItem getMasterItem() {
return this.masterItem;
}
}
And in my DAO code - I have these methods:
public MasterItem load(int id) {
MasterItem results = (MasterItem) getSessionFactory().getCurrentSession()
.get("com.xxx.MasterItem", id);
}
public void save(MasterItem master) {
// master has been changed by the UI since it
getSessionFactory().getCurrentSession().saveOrUpdate(master);
}
When I load a MasterItem, it loads correctly, and also loads the CriticalItems Set with data, as directed. Then, I send this data to my UI, and get an updated copy back, which I then try to persist. The user updates fields in the MasterItem object, but does not touch the CriticalItems Set or anything in it - it remains unmodified.
When my save() method is invoked, Hibernate is insisting on sending SQL updates for each item in the Set of CriticalItems, even though none of them have changed in any way.
After some digging, here's what I think is happening. When I do a saveOrUpdate(), Hibernate sees my MasterItem object is in a detached state, so it tries to reload it from disk. However, when doing so, it appears to be using a prepared statement (which was auto-created by Hibernate at start-up) and this prepared statement does not attempt to join to the CriticalItems data.
So Hibernate has my updated MasterItem object with a full Set of CriticalItems, but uses a MasterItem without collections as its "previousState" object. Thus, all CriticalItems get updated via SQL (not inserted, which is interesting in itself).
Did I do something in my annotations that caused this behavior? I know I can use an Interceptor to find out of the item has really changed, or changed the dirty flag to override Hibernate's default algorithm - but this seems to be something that Hibernate should just handle on its own without my intervention.
Any insight would be appreciated.
UPDATE: Based on comments, I think I understand the difference between saveOrUpdate() and merge(). I realize that saveOrUpdate() will result in either an SQL INSERT or an SQL UPDATE in all cases, and that merge, in theory, will only issue updates if the object has changed from it's persistent state, but in order to determine that, Hibernate has to reload the object first via SQL SELECT.
So, I thought I could just go back into my code and change saveOrUpdate() to merge() and it would work, but that wasn't quite the case.
When I used merge(), I was getting
org.springframework.orm.hibernate3.HibernateSystemException: could not initialize proxy - no Session; nested exception is org.hibernate.LazyInitializationException: could not initialize proxy - no Session
but it worked fine if I changed back to saveOrUpdate().
I finally found out why - I didn't include CascadeType.MERGE
in my @Cascade
annotation (ugh). Once I fixed that, the exception went away.
That's the semantical difference between update()
and merge()
.
From Christian Bauer and Gavin King's Java Persistence with Hibernate (I can't find clear explanation of this behaviour in the Hibernate docs):
The update() method forces an update to the persistent state of the object in the database, always scheduling an SQL UPDATE.
...
It doesn’t matter if the item object is modified before or after it’s passed to update().
...
Hibernate always treats the object as dirty and schedules an SQL UPDATE., which will be executed during flush.
On the other hand, merge()
queries the database first, and doesn't perform update if state haven't changed.
So, if you want Hibernate to query the database first, you need to use merge()
(though default behaviour of update()
can be overriden by specifing @org.hibernate.annotations.Entity(selectBeforeUpdate = true)
on your entities).