In case it helps anyone,

   The workaround I came up with was to run some code like this after
I generated the mappings, but before I use them:

        private static void FixNullVersionPropertiesForClass(
            ISession session, PersistentClass mapping)
        {
            if (mapping.IsVersioned)
            {
                var command = session.Connection.CreateCommand();
                var dialect = session.GetDialect();
                var versionPropertyName =
                    mapping.Version.ColumnIterator.First().GetText(dialect);
                string tableName = mapping.Table.GetQualifiedName(dialect);

                command.CommandText =
                    "update " + tableName +
                    " set " + versionPropertyName + " = 1 " +
                    "where " + versionPropertyName + " is null";

                session.Transaction.Enlist(command);

                command.ExecuteNonQuery();
            }
        }

   I just iterate through all the mappings in the configuration and
run this to fix things up. (Note, the session must have an open
transaction that will be committed outside this function.) An ideal
solution would also add NOT NULL constraints. (and never run more than
once)

Regards,
Mike

On Wed, Nov 17, 2010 at 3:22 PM, Mike Pontillo <[email protected]> wrote:
> Hi Fabio,
>
>   In my case, .Seed() returns 1. The problem is that version columns
> *already stored* (in the legacy DB I am mapping) have "null" values in
> them, not rows newly created by NHibernate.
>
>   At this point, I think the path of least resistance would be to
> write some SQL to update all the tables, before I start using
> NHibernate with this database. (I was hoping NHibernate could handle
> this for me seamlessly, but if version properties are already NULL in
> the database, I start running into this bug.)
>
> Regards,
> Mike
>
> On Wed, Nov 17, 2010 at 3:14 PM, Fabio Maulo <[email protected]> wrote:
>> That is because the Seed should return a valid value and not a null.
>> For integer based entities the Seed returns 1, that mean that a null
>> can't be the previous value stored for version. A null can be a value
>> for "unsaved" but not the actual value for a stored state.
>>
>> --
>> Fabio Maulo
>>
>>
>> El 17/11/2010, a las 20:09, Mike Pontillo <[email protected]> escribió:
>>
>>>   I have confirmed that this is the problem. (there "where" clause on
>>> the update can't handle a NULL version property) I see that in another
>>> part of AbstractEntityPersister.cs, this case is handled; for example:
>>>
>>>        updateBuilder.AddWhereFragment(_propertyColumnNames[k] + " is
>>> null");
>>>
>>>   I tried changing the code to handle this case, but the change
>>> quickly got much larger than I wanted. There seems to be the
>>> assumption (in several places) that the version property can never be
>>> null, including AbstractEntityPersister.GenerateUpdateString(), which
>>> is called at init time and [seems to?] cache the SQL for doing an
>>> update. This includes the "where" clause which assumes that the
>>> version property is not null.
>>>
>>>   There is also a call to SetVersionColumn() in the
>>> UpdateLockingStrategy class (GenerateLockString()) that I'm not sure
>>> what to do with, as this would also depend on the value of the version
>>> property.
>>>
>>>   I am guessing it was done this way for performance reasons? If
>>> every update needed to check for this obscure corner case, it might
>>> become significant.
>>>
>>> Regards,
>>> Mike
>>>
>>>
>>> On Nov 17, 11:49 am, Mike Pontillo <[email protected]> wrote:
>>>> Thanks,
>>>>
>>>>    Yes, it seems I will need a custom type, if only for the null-safe
>>>> .Next() method. I looked at the interface definition and didn't see
>>>> anything else that I really need to override.
>>>>
>>>>    As for multiple unsaved-values, I checked the IsUnsaved() method in
>>>> the VersionValue class and noticed that it checks for either null, or
>>>> the value. So I think that case is covered if I set unsaved-value="0"
>>>> in the XML.
>>>>
>>>>    Now the problem I am running into is different: the version update
>>>> fails. When I show the SQL I see something like this:
>>>>
>>>> NHibernate: UPDATE dbo.[EXAMPLE] SET [VERSION] = @p0 WHERE [ID] = @p1
>>>> AND [VERSION] = @p2;@p0 = 1 [Type: Int32 (0)], @p1 = 42 [Type: Decimal
>>>> (0)], @p2 = NULL [Type: Int32 (0)]
>>>>
>>>>    ... then NHibernate notices that while it expected one row to be
>>>> updated, zero rows were updated. (see stack trace below my signature)
>>>>
>>>>    I'm not sure if this is the real problem, but shouldn't the UPDATE
>>>> in this case be generated with "VERSION is NULL", not a "VERSION =
>>>> <some-value>"? In the SQL I see VERSION = @p2, where @p2 is the NULL
>>>> Int32 object. I thought that the NullSafeSet() was intended to handle
>>>> this case (called in ForceVersionIncrement() in
>>>> AbstractEntityPersister) but I wasn't sure how this happens. (Does
>>>> .NET take care of changing this to an "is NULL" statement?)
>>>>
>>>>    I ran the same SQL manually and it updates the row, but only if I
>>>> write "is null".
>>>>
>>>> Regards,
>>>> Mike
>>>>
>>>> NHibernate.StaleObjectStateException : Row was updated or deleted by
>>>> another transaction (or unsaved-value mapping was incorrect):
>>>> [MyCompany.Example#42]
>>>>   ----> NHibernate.StaleStateException : Unexpected row count: 0; 
>>>> expected: 1
>>>> at NHibernate.Persister.Entity.AbstractEntityPersister.Check(Int32
>>>> rows, Object id, Int32 tableNumber, IExpectation expectation,
>>>> IDbCommand statement) in AbstractEntityPersister.cs: line 2174
>>>> at 
>>>> NHibernate.Persister.Entity.AbstractEntityPersister.ForceVersionIncrement(Object
>>>> id, Object currentVersion, ISessionImplementor session) in
>>>> AbstractEntityPersister.cs: line 1618
>>>> at 
>>>> NHibernate.Event.Default.AbstractLockUpgradeEventListener.UpgradeLock(Object
>>>> entity, EntityEntry entry, LockMode requestedLockMode,
>>>> ISessionImplementor source) in AbstractLockUpgradeEventListener.cs:
>>>> line 64
>>>> at NHibernate.Event.Default.DefaultLockEventListener.OnLock(LockEvent
>>>> event) in DefaultLockEventListener.cs: line 55
>>>> at NHibernate.Impl.SessionImpl.FireLock(LockEvent lockEvent) in
>>>> SessionImpl.cs: line 2470
>>>> at NHibernate.Impl.SessionImpl.Lock(Object obj, LockMode lockMode) in
>>>> SessionImpl.cs: line 776
>>>> --StaleStateException
>>>> at 
>>>> NHibernate.AdoNet.Expectations.BasicExpectation.VerifyOutcomeNonBatched(Int32
>>>> rowCount, IDbCommand statement) in Expectations.cs: line 33
>>>> at NHibernate.Persister.Entity.AbstractEntityPersister.Check(Int32
>>>> rows, Object id, Int32 tableNumber, IExpectation expectation,
>>>> IDbCommand statement) in AbstractEntityPersister.cs: line 2163
>>>>
>>>> On Wed, Nov 17, 2010 at 10:54 AM, Fabio Maulo <[email protected]> wrote:
>>>>> ah... The Versioning is not the place of the modification, instead, the
>>>>> right place is the implementation of IVersionType inside Int32Type.
>>>>> btw... a IUserVersionType would be perfect for your case.
>>>>
>>>>> On Wed, Nov 17, 2010 at 3:44 PM, Fabio Maulo <[email protected]> wrote:
>>>>
>>>>>> The implementation of a custom type is not an overkill, instead it is 
>>>>>> just
>>>>>> the way NH gives you to define what is correct/intelligent for you.
>>>>>> For me Int32 is more than enough and its default "unsaved-value" as zero
>>>>>> is more than enough. You are looking for a version with two possible
>>>>>> unsaved-value ('null' and zero)
>>>>>> On Tue, Nov 16, 2010 at 9:14 PM, Mike Pontillo <[email protected]> 
>>>>>> wrote:
>>>>
>>>>>>> Hi Fabio,
>>>>
>>>>>>>   Thanks for the response. A custom type seemed like overkill here
>>>>>>> since all I really want is a nullable Int32. I ended up doing three
>>>>>>> things to work around this problem:
>>>>
>>>>>>>  - Made the version property in my POCO nullable (int?) to solve the
>>>>>>> problem where NHibernate found a "dirty" (but not really dirty) object
>>>>>>> in the database, since its version property was mistakenly set to null
>>>>>>>  - Made the version property in my mapping XML just an "int" (not sure
>>>>>>> if it necessary to call out that it's nullable in the mapping XML, but
>>>>>>> I got the exception noted below when I did -- it works when I leave
>>>>>>> out the type as well, of course.)
>>>>>>>  - Made the following code change (to fix the NullReferenceException
>>>>>>> when NHibernate tries to increment the null value in the database):
>>>>
>>>>>>> --- Versioning.cs
>>>>>>> +++ Versioning.cs       (working copy)
>>>>>>> @@ -28,6 +28,11 @@
>>>>>>>            /// <returns>Returns the next value for the 
>>>>>>> version.</returns>
>>>>>>>            public static object Increment(object version,
>>>>>>> IVersionType versionType, ISessionImplementor session)
>>>>>>>            {
>>>>>>> +            if(version == null)
>>>>>>> +            {
>>>>>>> +                version = versionType.Seed(session);
>>>>>>> +            }
>>>>>>> +
>>>>>>>            object next = versionType.Next(version, session);
>>>>>>>            if (log.IsDebugEnabled)
>>>>>>>            {
>>>>
>>>>>>>   By the way, I also tested to verify that the 3rd change was really
>>>>>>> necessary. (Since I saw Seed() being used elsewhere, I wasn't sure if
>>>>>>> it would try again to increment a null value after I fixed my
>>>>>>> mappings.)
>>>>
>>>>>>>   Also, in case anyone else is seeing this problem, the following
>>>>>>> exception is thrown when I try to specify the type of the version
>>>>>>> property in the XML as "int?":
>>>>
>>>>>>> NHibernate.MappingException : Could not compile the mapping document:
>>>>>>> Example.hbm.xml
>>>>>>>  ----> NHibernate.MappingException : Could not determine type for:
>>>>>>> MyCompany.Model.int?, PROLIN.DAO, for columns:
>>>>>>> NHibernate.Mapping.Column(VERSION)
>>>>>>> at NHibernate.Cfg.Configuration.LogAndThrow(Exception exception) in
>>>>>>> Configuration.cs: line 340
>>>>>>> at NHibernate.Cfg.Configuration.AddDeserializedMapping(HbmMapping
>>>>>>> mappingDocument, String documentFileName) in Configuration.cs: line
>>>>>>> 528
>>>>>>> at NHibernate.Cfg.Configuration.AddValidatedDocument(NamedXmlDocument
>>>>>>> doc) in Configuration.cs: line 497
>>>>>>> at NHibernate.Cfg.Configuration.ProcessMappingsQueue() in
>>>>>>> Configuration.cs: line 1830
>>>>>>> at NHibernate.Cfg.Configuration.AddDocumentThroughQueue(NamedXmlDocument
>>>>>>> document) in Configuration.cs: line 1821
>>>>>>> at NHibernate.Cfg.Configuration.AddXmlReader(XmlReader hbmReader,
>>>>>>> String name) in Configuration.cs: line 1814
>>>>>>> at NHibernate.Cfg.Configuration.AddInputStream(Stream xmlInputStream,
>>>>>>> String name) in Configuration.cs: line 644
>>>>>>> at NHibernate.Cfg.Configuration.AddResource(String path, Assembly
>>>>>>> assembly) in Configuration.cs: line 682
>>>>>>> at NHibernate.Cfg.Configuration.AddAssembly(Assembly assembly) in
>>>>>>> Configuration.cs: line 761
>>>>>>> --MappingException
>>>>>>> at NHibernate.Mapping.SimpleValue.get_Type() in SimpleValue.cs: line 241
>>>>>>> at NHibernate.Cfg.XmlHbmBinding.RootClassBinder.BindProperty(HbmVersion
>>>>>>> versionSchema, Property property, IDictionary`2 inheritedMetas) in
>>>>>>> RootClassBinder.cs: line 227
>>>>>>> at NHibernate.Cfg.XmlHbmBinding.RootClassBinder.BindVersion(HbmVersion
>>>>>>> versionSchema, PersistentClass rootClass, Table table, IDictionary`2
>>>>>>> inheritedMetas) in RootClassBinder.cs: line 209
>>>>>>> at NHibernate.Cfg.XmlHbmBinding.RootClassBinder.Bind(HbmClass
>>>>>>> classSchema, IDictionary`2 inheritedMetas) in RootClassBinder.cs: line
>>>>>>> 55
>>>>>>> at 
>>>>>>> NHibernate.Cfg.XmlHbmBinding.MappingRootBinder.AddRootClasses(HbmClass
>>>>>>> rootClass, IDictionary`2 inheritedMetas) in MappingRootBinder.cs: line
>>>>>>> 83
>>>>>>> at
>>>>>>> NHibernate.Cfg.XmlHbmBinding.MappingRootBinder.AddEntitiesMappings(HbmMapping
>>>>>>> mappingSchema, IDictionary`2 inheritedMetas) in MappingRootBinder.cs:
>>>>>>> line 42
>>>>>>> at NHibernate.Cfg.XmlHbmBinding.MappingRootBinder.Bind(HbmMapping
>>>>>>> mappingSchema) in MappingRootBinder.cs: line 29
>>>>>>> at NHibernate.Cfg.Configuration.AddDeserializedMapping(HbmMapping
>>>>>>> mappingDocument, String documentFileName) in Configuration.cs: line
>>>>>>> 520
>>>>
>>>>>>>   This exception may be an error on my part rather than a bug, since
>>>>>>> I am not explicitly calling out nullable properties anywhere else in
>>>>>>> my mapping XML. (I imagine I was trying to do something unsupported,
>>>>>>> but I don't explicitly state types anywhere else in my mapping XML.)
>>>>>>> The quirk here is that for a version property (according to the
>>>>>>> reference manual I found at
>>>>>>> http://www.nhforge.org/doc/nh/en/index.html, section 5.1.7), the
>>>>>>> "type" parameter is "(optional - defaults to Int32)". I'm not sure why
>>>>>>> this wouldn't default to the type defined in the POCO for the version
>>>>>>> property. Also, the manual states "Version numbers may be of type
>>>>>>> Int64, Int32, Int16, Ticks, Timestamp, or TimeSpan (or their nullable
>>>>>>> counterparts in .NET 2.0)", so I assumed I could write "int?".
>>>>
>>>>>>> Regards,
>>>>>>> Mike
>>>>
>>>>>>> On Thu, Nov 11, 2010 at 6:12 AM, Fabio Maulo <[email protected]>
>>>>>>> wrote:
>>>>>>>> May be you have to know that you can implements your own type for the
>>>>>>>> version property following the rules of your legacy DB.
>>>>
>>>>>>>> --
>>>>>>>> Fabio Maulo
>>>>
>>>>>>>> El 10/11/2010, a las 20:04, Mike Pontillo <[email protected]>
>>>>>>>> escribió:
>>>>
>>>>>>>>> In case anyone was wondering,
>>>>
>>>>>>>>>   I figured out the problem. I am trying to map a legacy database
>>>>>>>>> that uses version columns that are nullable. NHibernate threw
>>>>>>>>> exceptions when I defined the nullable types in my POCOs and the
>>>>>>>>> NHibernate XML (even though the manual stated that they were supported
>>>>>>>>> -- so likely a bug) so I defined them as "int". However, I didn't
>>>>>>>>> notice that some rows (very few - likely mistakes, or cases where
>>>>>>>>> someone hand-edited the data) in the database indeed have NULL values
>>>>>>>>> for the version column. If a query ever cached one of these NULL
>>>>>>>>> values, and NHibernate subsequently performed a dirty check, it
>>>>
>>>> ...
>>>>
>>>> read more »
>>>
>>> --
>>> You received this message because you are subscribed to the Google Groups 
>>> "nhusers" group.
>>> To post to this group, send email to [email protected].
>>> To unsubscribe from this group, send email to 
>>> [email protected].
>>> For more options, visit this group at 
>>> http://groups.google.com/group/nhusers?hl=en.
>>>
>>
>> --
>> You received this message because you are subscribed to the Google Groups 
>> "nhusers" group.
>> To post to this group, send email to [email protected].
>> To unsubscribe from this group, send email to 
>> [email protected].
>> For more options, visit this group at 
>> http://groups.google.com/group/nhusers?hl=en.
>>
>>
>

-- 
You received this message because you are subscribed to the Google Groups 
"nhusers" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/nhusers?hl=en.

Reply via email to