NHibernate/FluentNHibernate property bag

2019-02-18 07:52发布

问题:

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 = ''

回答1:

  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>


回答2:

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();
  }
}


回答3:

First, create a separate class for the composite PK:

public class VehiclePropertyPK
{
  public virtual Vehicle PropertyVehicle { set; get; }
  public virtual string Name { set; get; }

  // You must implement your own GetHasCode and Equals methods
  public override int GetHashCode()
  {
    ...
  }

  public override bool Equals(object o)
  {
    ...
  }
}

Then, refactor the VehicleProperty class this way:

public class VehicleProperty
{
  public virtual VehiclePropertyPK VehicleCompId { get; set; }
  public virtual string Value { get; set; }
}

Finally, the maps:

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

public class VehiclePropertyMap : ClassMap<VehicleProperty>
{
  public VehiclePropertyMap()
  {
    CompositeId<VehiclePropertyPK>(x => x.VehicleCompId)
            .KeyReference(y => y.PropertyVehicle , "Vehicle_Id")
            .KeyProperty(y => y.Name, "Name");
    Map(x => x.Value);
  }
}

(NHibernate 3.3.1 and FluentNHibernate 1.3.0)