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 will
throw this exception.
If I have time, I'll write up a test case and try patching the code
so NHibernate supports nullable version columns better. I think if
NHibernate treated NULL version columns as if they had the value 0,
this would fix the problem.
Regards,
Mike
--
Fabio Maulo
> --
> You received this message because you are subscribed to the Google Groups "nhusers" group.
> To post to this group, send email to nhu...@googlegroups.com.
> To unsubscribe from this group, send email to nhusers+u...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/nhusers?hl=en.
>
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
Ignore the "fixed" code, it was still half-baked, and there were a
few problems with it. First, I didn't notice that
AbstractEntityPersister.ForceVersionIncrement() also would need this
same check, and that the version update would actually fail (because
WHERE version = 0 isn't the same as WHERE version is null).
Next I tried another approach (change all the .Next() methods on
the integer types -- see attached patch) but that causes a different
exception. (see below my signature).
I think the real fix involves the default unsaved-value being both
NULL and 0 for nullable types (or a special case somewhere around
this), which I'm not sure how to accomplish...
Regards,
Mike
failed: NHibernate.StaleObjectStateException : Row was updated or
deleted by another transaction (or unsaved-value mapping was
incorrect): [MyCompany.Example#42]
Persister\Entity\AbstractEntityPersister.cs(2172,0): at
NHibernate.Persister.Entity.AbstractEntityPersister.Check(Int32 rows,
Object id, Int32 tableNumber, IExpectation expectation, IDbCommand
statement)
Persister\Entity\AbstractEntityPersister.cs(1618,0): at
NHibernate.Persister.Entity.AbstractEntityPersister.ForceVersionIncrement(Object
id, Object currentVersion, ISessionImplementor session)
Event\Default\AbstractLockUpgradeEventListener.cs(64,0): at
NHibernate.Event.Default.AbstractLockUpgradeEventListener.UpgradeLock(Object
entity, EntityEntry entry, LockMode requestedLockMode,
ISessionImplementor source)
Event\Default\DefaultLockEventListener.cs(55,0): at
NHibernate.Event.Default.DefaultLockEventListener.OnLock(LockEvent
event)
Impl\SessionImpl.cs(2470,0): at
NHibernate.Impl.SessionImpl.FireLock(LockEvent lockEvent)
Impl\SessionImpl.cs(776,0): at
NHibernate.Impl.SessionImpl.Lock(Object obj, LockMode lockMode)
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
--
Fabio Maulo
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
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