views:

858

answers:

2

Given a Vehicle class and a VehicleProperty class...

public class Vehicle
{
    public virtual int Id { get; protected set; }
    public virtual string Registration { get; set; }

    private List<VehicleProperty> _properties = new List<VehicleProperty>();
    public virtual IEnumerable<VehicleProperty> Properties
    {
        get { return _properties; }
        protected set{ _properties = new List<VehicleProperty>(value);}
    }

    public virtual void AddProperty(string name, string value)
    {
        _properties.Add(new VehicleProperty {Name = name, Value = value});
    }
}

public class VehicleProperty
{
    public virtual string Name { get; set; }
    public virtual string Value { get; set; }
}

How can I map the two classes so that the VehicleProperty table has a composite key of [VehicleId] and [Name]. Vehicle would be an aggregate root (VehicleProperty is not accessed outside of a Vehicle class).

I have tried everything that I can think of (I'm new to NHibernate so that's not much)

public class VehicleMap : ClassMap<Vehicle>
{
    public VehicleMap()
    {
        Id(x => x.Id);
        Map(x => x.Registration);
        HasMany(x => x.Properties)
            .Inverse()
            .Cascade.All();
    }
}

public class VehiclePropertyMap : ClassMap<VehicleProperty>
{
    public VehiclePropertyMap()
    {
        UseCompositeId()
            .WithKeyProperty(x => x.Name)
            .WithKeyReference(x => x.Vehicle, "Vehicle_Id");
        Map(x => x.Name);
        Map(x => x.Value);
    }
}

This mapping results in the below sql and a StaleStateException "Unexpected row count: 0; expected: 1" (I also don't really want to have a Vehicle property on the VehicleProperty)...

INSERT INTO "Vehicle" (Registration) VALUES (@p0); select last_insert_rowid(); @p0 = 'AA09CDE'
UPDATE "VehicleProperty" SET Name = @p0, Value = @p1 WHERE Name = @p2 AND Vehicle_Id = @p3; @p0 = 'Colour', @p1 = 'Black', @p2 = 'Colour', @p3 = ''
+3  A: 
  1. Don't use composite id's.
  2. Inverse means, that this is the inverse relation of an other one. If there is no other one, it's just not updated.
  3. You declared the VehicleProperty.Name Property as the primary key. if the primary key is already initialized, NH thinks it is already stored and therefore tries an update. (that's why you get the exception.) You can changed this behaviour, but it's better to use an artificial primary key or the mapping bellow.

I don't know FluentNHibernate to show you the code. I can tell you how it looks in XML.

<class name="Vehicle">
  <id name="Id" generator="native"/>

  <property name="Registration"/>

  <!-- declare the table Vehicle_Properties for the property Properties -->
  <bag name="Properties" table="Vehicle_Properties" cascade="all-delete-orphan">

    <!-- primary key of Vehicle_Properties and foreign key -->
    <key column="Vehicle_FK"/>

    <!-- contents of the table: Name and Value -->
    <composite-element class="VehicleProperty">
      <property name="Name"/>
      <property name="Value"/>
    </composite-element>
  </bag>

</class>
Stefan Steinegger
+5  A: 

I agree entirely with Stefan's points, and although I can't attest to the correctness of his mapping, the literal translation into Fluent NHibernate is as follows:

public class VehicleMap : ClassMap<Vehicle>
{
  public VehicleMap()
  {
    Id(x => x.Id);
    Map(x => x.Registration);

    HasMany(x => x.Properties)
      .Component(c =>
      {
        c.Map(x => x.Name);
        c.Map(x => x.Value);
      })
      .Cascade.AllDeleteOrphan();
  }
}
James Gregory