Hi,
I'm using NHibernate with ActiveRecord from CastleProject in a WinForms app. I've got an object graph that I'm trying to save.
The graph (see mapping below) is a Status together with its translations. In one session it is loaded and dynamically binded to a DataGridView (the first column is the original and the sebsequent are the translations). The user then operates on the detached status collection.
When I try to save (in another session, using optimistic locking) I sometimes get a StaleObjectStateException. This is OK because some other user might have changed the collection. I catch this exception and let the user decide whether to overwrite or discard his changes. Let's say he/she wants to overwrite with his/her changes. The problem is that after the first failing update the objects that were newly created by the user have their Id and Version set to some generated values. I think that these values should be reverted by NH, because otherwise I cannot insert them to the DB during the subsequent try. Id and Version is already set so NH will think the entity should exist in the DB and tries to update (actually pre-select them first 'cause I use select-before-update), it fails to find them and throws StaleObjectStateException again.
Below a sample with a one-element collection of statuses and its translations (in brackets):
A [AA, <empty>]
user X and Y open a form and start editing the values.
User Y edits AA to AB and saves the changes. Everything's ok.
User X inserts "AAA" into <empty> cell (which creates a new Status entity and setts its Parent together with attaching it to the Parent's Translations). I want to save the whole A entity, but I get an exception, cause it's stale. It's ok. I'm asking the user what to do, and he wants to use HIS data. I fetch current data from the database (A [AB, <empty>]) and change AA to AB and add the newly translation created by the user X (AAA). I want to save:
A [AA, AAA]
When I try to save the whole graph (saving just A), it throw an exception cause my AAA object has already an Id and a Value set by the NH when it tried to save it first time.
I can work-around it by manually setting Id and Version of AAA to 0 but this is something error-prone since I must decide whether the entity was newly created or not.
Below the exact code and mapping.
Hibernate version: 1.2.0; modified (see my post: http://forum.hibernate.org/viewtopic.php?p=2395324#2395324)
Mapping documents: I use ActiveRecord and my class is:
The TypeDescriptorProvider is used to autocreate columns in DataGridView from the Translations.
Code:
using System;
using System.Collections.Generic;
using System.Text;
using Castle.ActiveRecord;
using System.ComponentModel;
namespace X.Y.Z
{
[ActiveRecord(SelectBeforeUpdate=true)]
[System.Diagnostics.DebuggerDisplay("{Value}, language: {Lang}")]
[TypeDescriptionProvider(typeof(DynamicProperties.StatusTypeDescriptionProvider))]
[Localizable(true)]
public class Status : MainActiveRecordBase<Status>
{
private int _id;
private Language _lang;
private Status _parent;
private IList<Status> _translations = new List<Status>();
private string _value;
private int _version;
[PrimaryKey(PrimaryKeyType.Native)]
public int Id
{
get
{
return _id;
}
set
{
_id = value;
}
}
[BelongsTo("LangId")]
public Language Lang
{
get
{
return _lang;
}
set
{
_lang = value;
}
}
[BelongsTo("ParentId")]
public Status Parent
{
get
{
return _parent;
}
set
{
_parent = value;
}
}
[HasMany(Cascade=ManyRelationCascadeEnum.All, Inverse=true, Sort="natural")]
public IList<Status> Translations
{
get
{
return _translations;
}
set
{
_translations = value;
}
}
[Property(Length=200)]
[Localizable(true)]
public string Value
{
get
{
return _value;
}
set
{
_value = value;
}
}
[Version()]
public int Version
{
get
{
return _version;
}
set
{
_version = value;
}
}
public override bool Equals(object obj)
{
if (object.ReferenceEquals(this, obj))
{
return true;
}
Status other = obj as Status;
if (other == null)
{
return false;
}
return _id == other._id;
}
public override int GetHashCode()
{
return _id.GetHashCode();
}
public static IEnumerable<Status> FindRoots()
{
//this method is defined by ActiveRecord, and it means:
//Fetch all with null Parent and order by Value
return Status.FindAllByProperty("Value", "Parent", null);
}
}
}
Code between sessionFactory.openSession() and session.close():I first try to Save all the changes:
Code:
public void ApplyChanges()
{
using (TransactionScope scope = new TransactionScope())
{
foreach (Status st in statusRoots)
{
st.Save();
}
foreach (Status st in toBeRemoved)
{
st.Delete();
}
scope.VoteCommit();
scope.Flush();
}
}
If it fails, and the user wants to overwrite the changes made concurently by someone else I run:
Code:
public void SaveMyVersion()
{
foreach (Status st in statusRoots)
{
try
{
using (TransactionScope scope = new TransactionScope())
{
st.Save();
scope.VoteCommit();
scope.Flush();
}
}
catch (StaleObjectStateException ex)
{
using (TransactionScope scope = new TransactionScope())
{
//other session (user) modified (not deleted) the object
if (Status.Exists<int>(st.Id))
{
Status newVal = Status.Find(st.Id);
newVal.Value = st.Value;
foreach (Status trans in st.Translations)
{
if (!newVal.Translations.Contains(trans))
{
newVal.Translations.Add(trans);
trans.Parent = newVal;
//uncomment two lines below and it should work
//trans.Version = 0;
//trans.Id = 0;
}
else
{
Status newTrans = newVal.Translations[newVal.Translations.IndexOf(trans)];
newTrans.Value = trans.Value;
}
}
newVal.Save();
}
scope.VoteCommit();
scope.Flush();
}
}
}
foreach (Status st in toBeRemoved)
{
try
{
using (TransactionScope scope = new TransactionScope())
{
st.Delete();
scope.VoteCommit();
scope.Flush();
}
}
catch (StaleObjectStateException)
{
using (TransactionScope scope = new TransactionScope())
{
//other session (user) modified (not deleted) the object
if (Status.Exists<int>(st.Id))
{
Status newVal = Status.Find(st.Id);
newVal.Delete();
}
scope.VoteCommit();
scope.Flush();
}
}
}
}
Exception
Message:
Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) for Atmosphere.Reclamation.Logic.Status instance with identifier: 186
It happens in SessionImpl class, method FlushEntity:
currentPersistentState = persister.GetDatabaseSnapshot(entry.Id, entry.Version, this);
As described: it tries to get a snapshot even though it is a newly created entity. The behaviour in this method is OK, cause the class/methos found that the entity has non-default (non-zero) Id and Version.
Name and version of the database you are using:
Oracle 10g Express
The generated SQL (show_sql=true):
NHibernate: SELECT status0_.Id as Id0_2_, status0_.Version as Version0_2_, status0_.Value as Value0_2_, status0_.LangId as LangId0_2_, status0_.ParentId as ParentId0_2_, language1_.Id as Id1_0_, language1_.Iso3 as Iso2_1_0_, status2_.Id as Id0_1_, status2_.Version as Version0_1_, status2_.Value as Value0_1_, status2_.LangId as LangId0_1_, status2_.ParentId as ParentId0_1_ FROM Statuses status0_, Languages language1_, Statuses status2_ WHERE status0_.LangId=language1_.Id(+) and status0_.ParentId=status2_.Id(+) AND status0_.Id=:p0; :p0 = '186'
The 186 is the newcly created entity (AAA)
Am I doing it the wrong way or is it a NH bug and NH should revert Id and Version after failed insert attempt?