These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 1 post ] 
Author Message
 Post subject: Recovery from StaleObjectStateException - optimistic locking
PostPosted: Mon Sep 15, 2008 3:31 pm 
Newbie

Joined: Sun Sep 14, 2008 6:40 pm
Posts: 6
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?


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 1 post ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.