Issue
So, I have Class A and Class B.
They share their primary key, using the following configuration:
In Class A I reference Class B as a child
@OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@PrimaryKeyJoinColumn
public B getB()
{
return b;
}
In Class B, in order to get ID from parent Class A, I use the following annotations:
@Id
@GeneratedValue(generator = "customForeignGenerator")
@org.hibernate.annotations.GenericGenerator(name = "customForeignGenerator", strategy = "foreign", parameters = @org.hibernate.annotations.Parameter(name = "property", value = "a"))
@Column(name = "a_id")
public Long getId()
{
return id;
}
@MapsId("id")
@OneToOne(mappedBy = "b")
@PrimaryKeyJoinColumn
public A getA()
{
return a;
}
The problem is that uppon saving A with
session.saveOrUpdate(aInstance);
DB returns the following error:
Duplicate entry '123456' for key 'PRIMARY'
This tells us 2 things, first is that @MapsId
is working correctly, giving A's Id to B as it should, the second is that hibernate decided it was a 'save' and not an 'update', and this only happens on saveOrUpdate
when Id is null right? (wierd?)
The usual solution would be to get
the old B from DB and merge
, if existed, but that arrises a whole lot of problems like also getting the old A from DB to session or making the dreaded "a different object with the same identifier value was already associated with the session
" hibernate errors for the assossiated objects. Also not very performance friendly, doing unecessery DB hits.
Is there an error in my anotations? Am I doing it wrong? What is the normal configuration for this?
EDIT:
It kind of defeats the purpose of using @MapsId
, setting the IDs manually, but since no solution was found I did set the IDs manually like this:
if(aInstance.getId() != null)
aInstance.getB().setId(aInstance.getId());
session.saveOrUpdate(aInstance);
Just until moments ago this was returning the following error:
org.hibernate.StaleStateException:
Batch update returned unexpected row count from update: 0 actual row count: 0 expected: 1
But for some reason it stopped throwing the error and now it works. In all cases, the previous code is still all valid since aInstance
might not have Id, and in that case, the MapId works perfectly inserting a new A and B in BD. The problem was only on Update.
Was\Is it an hibernate bug? probably. I'll let you guys know when StaleStateException
turn up again.
For now this is a temporary solution, until someone comes up with the actual solution.
Solution
I finally found the answer to all the problems.
To understand the root of the problem we must remind how saveOrUpdate(object)
works.
1) If object
has ID set, saveOrUpdate
will Update
else it will Save
.
2) If hibernate decides it is a Save
but object is already on DB, you want to update, the Duplicate entry '123456' for key 'PRIMARY'
exception will occur.
3) If hibernate decides it is an Update
but object is not in DB, you want to save, the StaleStateException
Exception occurs.
The problem relies on the fact that if aInstance
exists in DB and already has an ID, @MapsId
will give that ID to B
ignoring the rules above, making Hibernate think B
also exists in DB when it may not. It only works properly when both A
and B
dont exist in DB or when they both exist.
Therefor the workaround solution is to make sure you Set
the ID only and only if each object exists in DB, and set ID to null when it does not:
B dbB = (B) unmarshaller.getDetachedSession().createCriteria(B.class).add(Restrictions.idEq(aInstance.getId())).uniqueResult();
if (dbB != null) //exists in DB
{
aInstance.getB().setId(aInstance.getId()); //Tell hibernate it is an Update
//Do the same for any other child classes to B with the same strategy if there are any in here
}
else
{
aInstance.getB().setId(null); //Tell hibernate it is a Save
}
unmarshaller.getDetachedSession().clear();
(using detached session, so that main session stays clear of unwanted objects, avoiding the "object with the same identifier in session
" exception)
If you dont need the DB object, and only want to know if it exists or not in the DB, you can use a Count, making it much lighter:
String query = "select count(*) from " + B.class.getName() + " where id = " + aInstance.getId();
Long count = DataAccessUtils.uniqueResult(hibernateTemplate.find(query));
if (count != null && count > 0)
{
aInstance.getB().setId(aInstance.getId()); // update
}
else
{
aInstance.getB().setId(null); // save
}
Now you can saveOrUpdate(aInstance);
But like i said, @MapsId
strategy is not very Hibernate friendly.
Answered By - Gotham Llianen
Answer Checked By - Willingham (JavaFixing Volunteer)