views:

69

answers:

2

Hello.

My schema looks like this:

database schema diagram

The Message.Id column is an identity(1,1), and the MessageHeader table uses a composite key consisting of the associated message ID, and a header name field that uniquely identifies a header within each message.

Entities are represented in code as:

public class Message {
    public virtual int Id { get; set; }
    public virtual string Sender { get; set; }
    public virtual string Recipient { get; set; }
    private IList<MessageHeader> headers = new List<MessageHeader>();

    public virtual IList<MessageHeader> Headers { 
        get { return headers; } 
        set { headers = value; }
    }

    public virtual void SetHeader(string headerName, string headerText) {
        MessageHeader header = this.Headers.FirstOrDefault(h => h.HeaderName == headerName);
        if (header == default(MessageHeader)) {
            header = new MessageHeader() {
                HeaderName = headerName,
                Message = this
            };
            this.Headers.Add(header);
            header.HeaderText = headerText;
        }
    }
}

public class MessageHeader {
    public virtual Message Message { get; set; }
    public virtual string HeaderName { get; set; }
    public virtual string HeaderText { get; set; }
    public virtual byte[] Version { get; set; }

    public override bool Equals(object obj) {
        if (Object.Equals(this, obj)) return (true);
        var that = obj as MessageHeader;
        if (that == null) return (false);
        return (this.HeaderName == that.HeaderName && this.Message.Equals(that.Message));
    }

    public override int GetHashCode() {
        return (this.HeaderName.GetHashCode() ^ this.Message.GetHashCode());
    }
}

I want to map these entities using Fluent NHibernate so that when I run code like:

var message = new Message() { Sender = "alf", Recipient = "bob" };
message.SetHeader("DateSent", DateTime.Now.ToString());

using (var session = NHibernateHelper.GetCurrentSession()) {
   using(var tx = session.BeginTransaction()) {
     session.Save(message);
     tx.Commit();
   }
}

NHibernate will:

  1. INSERT the Message entity and retrieve the identity value.
  2. INSERT the MessageHeader entity (with the appropriate MessageId retrieved in step 1)

I cannot make this work. It either tries to insert the MessageHeader first (which fails with a foreign key violation), or it tries to UPDATE the MessageHeader instead of INSERTing it, which returns no rows and throws a "could not synchronise database state with session" error.

My FluentNH mapping overrides are as follows:

public class MessageOverrides : IAutoMappingOverride<Message> {

    public void Override(AutoMapping<Message> map) {
        map.HasMany(message => message.Headers).KeyColumn("MessageId");
    }
}

public class MessageHeaderOverrides : IAutoMappingOverride<MessageHeader> {
    public void Override(AutoMapping<MessageHeader> map) {
        map.IgnoreProperty(header => header.Message);
        map.CompositeId()
            .KeyReference(header => header.Message, "MessageId")
            .KeyProperty(header => header.HeaderName);
        map.Version(header => header.Version)
            .CustomSqlType("timestamp").Nullable()
            .Generated.Always();
    }
}

I thought adding the explicit Version timestamp to the MessageHeader table would resolve this, but it would appear not... I'm sure something needs to be Inverse or Cascade or something, but I am completely stumped as to what needs to go where.

Thanks,

Dylan

A: 

This is a shot in the dark until I can get to a machine where I can reproduce your issue...

Should your HasMany not be using both columns to reference the headers?

Depending on your Fluent NHibernate version, it'll probably be:

map.HasMany(x=> x.Headers)
  .KeyColumns.Add("MessageId", "HeaderName");

or

map.HasMany(x=> x.Headers)
  .Key(ke =>
  {
     ke.Columns.Add("MessageId", "HeaderName");
  });
James Gregory
A: 

The solution turned out to be - via @mikehadlow on Twitter - that I need a .Cascade.SaveUpdate() on the HasMany mapping:

public void Override(AutoMapping<Message> map) {
    map.HasMany(message => message.Headers).KeyColumn("MessageId").Cascade.SaveUpdate()
}

but it also turned out that a non-NH-related bug elsewhere in the code was preventing this from working properly. Fixed the bug, mapped it as shown above, and it worked. The Version property on the MessageHeader is nice-to-have but not required - if you omit it, NH will query the DB before an insert/update to find out which one is required.

Dylan Beattie