I'm running into a problem with composite-elements using NHibernate 2.0.0.Alpha1.
Specifically, if I save an entity containing a list of composite-elements that contains an empty instance (i.e., all properties are null), then get the same entity in another session and try to replace the resulting null value in the list with an instance of the composite-element class, NHibernate tries to insert a new row with the existing index rather than update the existing row, resulting in a constraint violation. It doesn't matter if the replacement instance is empty or not; once an empty instance has been saved it seems like there is no way to change it.
Here's a sample program and mapping file (by the way, I'm using SQLite for my database, and I'm using .NET 3.5):
Code:
using System;
using System.Collections.Generic;
using System.Reflection;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;
namespace ConsoleApplication1
{
internal class Program
{
private static void Main()
{
try
{
Configuration cfg = new Configuration()
.SetProperties(new Dictionary<string, string>
{
{ "connection.provider", "NHibernate.Connection.DriverConnectionProvider" },
{ "connection.connection_string", "Data Source=Test.sqlite" },
{ "connection.driver_class", "NHibernate.Driver.SQLite20Driver" },
{ "dialect", "NHibernate.Dialect.SQLiteDialect" },
{ "show_sql", "true" }
}).AddAssembly(Assembly.GetExecutingAssembly());
new SchemaExport(cfg).Create(false, true);
ISessionFactory factory = cfg.BuildSessionFactory();
Guid id;
using (ISession session = factory.OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
var entity = new TestEntity();
entity.TestList.Add(new TestComponent());
id = (Guid)session.Save(entity);
transaction.Commit();
}
using (ISession session = factory.OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
var entity = session.Get<TestEntity>(id);
for (int i = 0; i < entity.TestList.Count; ++i)
{
if (entity.TestList[i] == null)
{
entity.TestList[i] = new TestComponent();
entity.TestList[i].Test1 = "Hello";
}
}
transaction.Commit();
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
Console.WriteLine("Done.");
Console.ReadLine();
}
}
public class TestEntity
{
private IList<TestComponent> testList;
public TestEntity()
{
testList = new List<TestComponent>();
}
public virtual Guid Id { get; set; }
public virtual IList<TestComponent> TestList
{
get { return testList; }
set { testList = value; }
}
}
public class TestComponent
{
public virtual string Test1 { get; set; }
public virtual string Test2 { get; set; }
}
}
Code:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="ConsoleApplication1" namespace="ConsoleApplication1" auto-import="true">
<class name="TestEntity" table="TestEntity">
<id name="Id" type="Guid">
<generator class="guid" />
</id>
<list name="TestList" table="TestList">
<key column="TestKey" />
<index column="TestIndex" />
<composite-element class="TestComponent">
<property name="Test1" type="String" />
<property name="Test2" type="String" />
</composite-element>
</list>
</class>
</hibernate-mapping>
And here's the output I get when I run this program:
Code:
NHibernate: INSERT INTO TestEntity (Id) VALUES (@p0); @p0 = 'fc0db3ce-2f0d-430a-ac9b-a76463c2cd4b'
NHibernate: INSERT INTO TestList (TestKey, TestIndex, Test1, Test2) VALUES (@p0, @p1, @p2, @p3); @p0 = 'fc0db3ce-2f0d-430a-ac9b-a76463c2cd4b', @p1 = '0', @p2 ='', @p3 = ''
NHibernate: SELECT testentity0_.Id as Id0_0_ FROM TestEntity testentity0_ WHERE testentity0_.Id=@p0; @p0 = 'fc0db3ce-2f0d-430a-ac9b-a76463c2cd4b'
NHibernate: SELECT testlist0_.TestKey as TestKey0_, testlist0_.Test1 as Test2_0_, testlist0_.Test2 as Test3_0_, testlist0_.TestIndex as TestIndex0_ FROM TestList testlist0_ WHERE testlist0_.TestKey=@p0; @p0 = 'fc0db3ce-2f0d-430a-ac9b-a76463c2cd4b'
NHibernate: INSERT INTO TestList (TestKey, TestIndex, Test1, Test2) VALUES (@p0, @p1, @p2, @p3); @p0 = 'fc0db3ce-2f0d-430a-ac9b-a76463c2cd4b', @p1 = '0', @p2 ='Hello', @p3 = ''
NHibernate.Exceptions.GenericADOException: could not insert collection rows: [ConsoleApplication1.TestEntity.TestList#fc0db3ce-2f0d-430a-ac9b-a76463c2cd4b][SQL: SQL not available] ---> System.Data.SQLite.SQLiteException: Abort due to constraint violation
columns TestKey, TestIndex are not unique
at System.Data.SQLite.SQLite3.Reset(SQLiteStatement stmt)
at System.Data.SQLite.SQLite3.Step(SQLiteStatement stmt)
at System.Data.SQLite.SQLiteCommand.ExecuteNonQuery()
at NHibernate.AdoNet.AbstractBatcher.ExecuteNonQuery(IDbCommand cmd)
at NHibernate.AdoNet.NonBatchingBatcher.AddToBatch(IExpectation expectation)
at NHibernate.Persister.Collection.AbstractCollectionPersister.InsertRows(IPersistentCollection collection, Object id, ISessionImplementor session)
--- End of inner exception stack trace ---
at NHibernate.Persister.Collection.AbstractCollectionPersister.InsertRows(IPersistentCollection collection, Object id, ISessionImplementor session)
at NHibernate.Action.CollectionUpdateAction.Execute()
at NHibernate.Engine.ActionQueue.Execute(IExecutable executable)
at NHibernate.Engine.ActionQueue.ExecuteActions(IList list)
at NHibernate.Engine.ActionQueue.ExecuteActions()
at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session)
at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event)
at NHibernate.Impl.SessionImpl.Flush()
at NHibernate.Transaction.AdoTransaction.Commit()
at ConsoleApplication1.Program.Main() in C:\Documents and Settings\mdavis\Local Settings\Application Data\Temporary Projects\ConsoleApplication1\Program.cs:line 48
Done.
Am I doing something wrong here?
Also, is there any way I can override NHibernate's default behavior of returning a null reference for composite-elements whose fields are all null so that it would return an empty instance instead? I've got an IInterceptor that seems to be doing the job for components that aren't in a collection by doing the replacement in OnLoad(), but I'm not quite sure how or if I can do this for collections.
Thanks.