Anshul
Anshul

Reputation: 1382

NHibernate trying to Insert an Existing Record for FK Mapping but only when int ID is 0

I'm working on a messaging system and in it there is an object called Buffer that stores incoming messages and their locations (for message tracking) as the message is travelling through the system. I am running into some issues that point to something fundamentally wrong with either my application or with NHibernate. I'm using Fluent NHibernate for my mappings.

I have two objects: Buffer and BufferMessageLocation. Buffer is a table in the database and each record represents a buffer message (which are represented by an instance of the Buffer class in code). BufferMessageLocation is essentially a lookup value that can have 5 different values: 0-4 (each of which represent a location). Each Buffer object contains a BufferMessageLocation. BufferMessageLocation is is represented as a Table[int ID, string Name] in the database.

I have to do some detached entity processing because the lifespan of a buffer message in my app will be longer than an NHibernate Session's lifespan.

This means:

  1. Open new session, go get the buffer message from the DB, close the session
  2. Do some processing in my app which updates the buffer message's location to, let's say, {ID=0, Name="TestLocation"}.
  3. Open new session, get the new location by calling Session.Load< BufferMessageLocation >(0), close the session.
  4. Update the buffer message's BufferMessageLocation to the location retrieved in step 3.
  5. Open new session, call Session.Merge(buffer) on the buffer message, close the session.

From all that I've read about NHibernate, this should work. However, for some reason, NHibernate is trying to INSERT a new record into BufferMessageLocation, instead of just updating the Buffer record.

Here's the error message:

could not insert: [AutomationBase.RepositoryNS.DataAccess.NHMG.Fluent.DTO.DBBufferMessageLocation][SQL: INSERT INTO tblBufferMessageLocation (Name) VALUES (?); select SCOPE_IDENTITY()]    

The InnerException is:

Cannot insert the value NULL into column 'ID', table 'AutomationNew.dbo.tblBufferMessageLocation'; column does not allow nulls. INSERT fails.\r\nThe statement has been terminated.

The SQL that is generated that throws this error is:

NHibernate: INSERT INTO tblBufferMessageLocation (Name) VALUES (@p0); select SCOPE_IDENTITY();@p0 = 'TestLocation' [Type: String (4000)]

So it looks like it's trying to insert a new BufferMessageLocation with Name="TestLocation" (which already exists) and ID=null whenever I update the buffer's location to that value. There are two things wrong with this scenario:

  1. Why is it inserting a new record when I am simply referencing a record that I just got from the database?

  2. Why is it not providing a value for the ID of the BufferMessageLocation when inserting the new record?

It's worth noting that this only happens when I'm updating the location to ID=0. I've tried changing the ID of the Name="TestLocation" record to 1,2,3,4 and 5, and they all work. Pretty much every value for ID works except for 0. I've even tried moving that record to ID=5, inserting a new BufferMessageLocation record with ID=0 and updating the buffer message's location to this new record, but the problem is still there. So it would seem that ID=0 is causing some issues with NHibernate's record detection mechanism, since all other int ID values work.

Another important thing to note is that this only happens when I use multiple sessions. If I do all of it in a single session, it all works fine. My guess is that Session.Merge, when called for merging detached entities, is not able to detect that there is already a BufferMessageLocation with ID=0.

If anyone has any idea about why this is happening and/or if this is a problem with NHibernate or something that I'm doing wrong, I'd appreciate the feedback. The entirety of my code follows.

Sorry for such a long post, but this is a very specific problem so I had to go into detail.

=========================================================================

APPLICATION CODE (please ignore the "DB" prefixes on class/object names, they are for separation of layers):

class Program
{
    static void Main(string[] args)
    {
        DBBuffer dbBuffer;
        DBInstruction instruction;
        DBBufferMessageLocation bufferMessageLocation;

        using (ISession session = FNHHelper.OpenSession())
        {
            using (ITransaction tran = session.BeginTransaction())
            {
                dbBuffer = session.QueryOver<DBBuffer>().List().First();
                instruction = session.Load<DBInstruction>(2);
                bufferMessageLocation = session.Load<DBBufferMessageLocation>(0);
                tran.Commit();
            }
        }

        //do application processing

        dbBuffer.BufferMessageLocation = bufferMessageLocation;

        //required attributes
        dbBuffer.Instruction = instruction;
        dbBuffer.Processed = false;
        dbBuffer.FromProducer = false;

        using (ISession session = FNHHelper.OpenSession())
        {
            using (ITransaction tran = session.BeginTransaction())
            {
                session.Merge(dbBuffer); <======== This is where the error is thrown
                tran.Commit();
            }
        }
    }
}

DTO CLASSES (Domain classes for this purpose):

public partial class DBBuffer : IRepositoryEntity 
{
    public System.Guid ID { get; set; }
    public DBLog Log { get; set; }
    public DBError Error { get; set; }
    public DBInstruction Instruction { get; set; }
    public DBBufferMessageLocation BufferMessageLocation { get; set; }
    public DateTime BufferTime { get; set; }
    public string ClientIP { get; set; }
    public string Description { get; set; }
    public bool Processed { get; set; }
    public bool FromProducer { get; set; }
}

public partial class DBBufferMessageLocation : IRepositoryLookup 
{
    public int ID { get; set; }
    public string Name { get; set; }
    public IList<DBBuffer> Buffers { get; set; }

    public DBBufferMessageLocation()
    {
        Buffers = new List<DBBuffer>();
    }
}

FLUENT MAPPINGS:

public partial class DBBufferMap : ClassMap<DBBuffer> {

    public DBBufferMap() {
        Table("tblBuffer");
        Id(x => x.ID).GeneratedBy.Assigned().Column("ID");
        References(x => x.Log).Column("LogID").Cascade.All();
        References(x => x.Error).Column("ErrorID").Cascade.All();
        References(x => x.Instruction).Column("InstructionID").Cascade.All();
        References(x => x.BufferMessageLocation).Column("BufferMessageLocationID").Cascade.All();
        Map(x => x.BufferTime).Column("BufferTime").Not.Nullable();
        Map(x => x.ClientIP).Column("ClientIP");
        Map(x => x.Description).Column("Description");
        Map(x => x.Processed).Column("Processed").Not.Nullable();
        Map(x => x.FromProducer).Column("FromProducer").Not.Nullable();
    }
}

public partial class DBBufferMessageLocationMap : ClassMap<DBBufferMessageLocation> {

    public DBBufferMessageLocationMap() {
        Table("tblBufferMessageLocation");
        Id(x => x.ID).GeneratedBy.Identity().Column("ID");
        Map(x => x.Name).Column("Name").Not.Nullable();
        HasMany(x => x.Buffers).KeyColumn("BufferMessageLocationID");
    }
}

Upvotes: 2

Views: 1294

Answers (1)

Firo
Firo

Reputation: 30803

Id == 0 is treated special as "Id not set" change the unset value to something different and initialize it in the constructor

public DBBufferMessageLocationMap()
{
    Id(x => x.ID, "ID").GeneratedBy.Identity().UnsavedValue(DBBufferMessageLocation.UNSET_ID);
}


public const int UNSET_ID = -1;
public DBBufferMessageLocation()
{
    Id = UNSET_ID;
}

Upvotes: 2

Related Questions