Taking a very simple example of one-to-many relationship (country ->
state).
Country (inverse side) :
@OneToMany(mappedBy = "country", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<StateTable> stateTableList=new ArrayList<StateTable>(0);
StateTable (owning side) :
@JoinColumn(name = "country_id", referencedColumnName = "country_id")
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH})
private Country country;
The method attempting to update a supplied (detached) StateTable
entity within an active database transaction (JTA or resource local) :
public StateTable update(StateTable stateTable) {
// Getting the original state entity from the database.
StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId());
// Get hold of the original country (with countryId = 67, for example).
Country oldCountry = oldState.getCountry();
// Getting a new country entity (with countryId = 68) supplied by the client application which is responsible for modifying the StateTable entity.
// Country has been changed from 67 to 68 in the StateTable entity using for example, a drop-down list.
Country newCountry = entityManager.find(Country.class, stateTable.getCountry().getCountryId());
// Attaching a managed instance to StateTable.
stateTable.setCountry(newCountry);
// Check whether the supplied country and the original country entities are equal.
// (Both not null and not equal - http://stackoverflow.com/a/31761967/1391249)
if (ObjectUtils.notEquals(newCountry, oldCountry)) {
// Remove the state entity from the inverse collection held by the original country entity.
oldCountry.remove(oldState);
// Add the state entity to the inverse collection held by the newly supplied country entity
newCountry.add(stateTable);
}
return entityManager.merge(stateTable);
}
It should be noted that orphanRemoval
is set to true
. The StateTable
entity is supplied by a client application which is interested in changing the entity association Country
(countryId = 67
) in StateTable
to something else (countryId = 68
) (thus on the inverse side in JPA, migrating a child entity from its parent (collection) to another parent (collection) which orphanRemoval=true
will in turn oppose).
The Hibernate provider issues a DELETE
DML statement causing the row corresponding to the StateTable
entity to be removed from the underlying database table.
Despite the fact that orphanRemoval
is set to true
, I expect Hibernate to issue a regularUPDATE
DML statement causing the effect of orphanRemoval
to be suspended in its entirely because the relationship link is migrated (not simply deleted).
EclipseLink does exactly that job. It issues an UPDATE
statement in the scenario given (having the same relationship with orphanRemoval
set to true
).
Which one is behaving according to the specification? Is it possible to make Hibernate issue an UPDATE
statement in this case other than removing orphanRemoval
from the inverse side?
This is only an attempt to make a bidirectional relationship more consistent on both the sides.
The defensive link management methods namely add()
and remove()
used in the above snippet, if necessary, are defined in the Country
entity as follows.
public void add(StateTable stateTable) {
List<StateTable> newStateTableList = getStateTableList();
if (!newStateTableList.contains(stateTable)) {
newStateTableList.add(stateTable);
}
if (stateTable.getCountry() != this) {
stateTable.setCountry(this);
}
}
public void remove(StateTable stateTable) {
List<StateTable> newStateTableList = getStateTableList();
if (newStateTableList.contains(stateTable)) {
newStateTableList.remove(stateTable);
}
}
Update :
Hibernate can only issue an expected UPDATE
DML statement, if the code given is modified in the following way.
public StateTable update(StateTable stateTable) {
StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId());
Country oldCountry = oldState.getCountry();
// DELETE is issued, if getReference() is replaced by find().
Country newCountry = entityManager.getReference(Country.class, stateTable.getCountry().getCountryId());
// The following line is never expected as Country is already retrieved
// and assigned to oldCountry above.
// Thus, oldState.getCountry() is no longer an uninitialized proxy.
oldState.getCountry().hashCode(); // DELETE is issued, if removed.
stateTable.setCountry(newCountry);
if (ObjectUtils.notEquals(newCountry, oldCountry)) {
oldCountry.remove(oldState);
newCountry.add(stateTable);
}
return entityManager.merge(stateTable);
}
Observe the following two lines in the newer version of the code.
// Previously it was EntityManager#find()
Country newCountry = entityManager.getReference(Country.class, stateTable.getCountry().getCountryId());
// Previously it was absent.
oldState.getCountry().hashCode();
If either the last line is absent or EntityManager#getReference()
is replaced by EntityManager#find()
, then a DELETE
DML statement is unexpectedly issued.
So, what is going on here? Especially, I emphasize portability. Not porting this kind of basic functionality across different JPA providers defeats the use of ORM frameworks severely.
I understand the basic difference between EntityManager#getReference()
and EntityManager#find()
.