views:

3165

answers:

4

Hello,

I'm using Fluent NHibernate version 1.0.0.579 (latest version at this date). I have an abstract Activity class and several inheriting classes, eg. DummyActivity. All of them use the same table Activities, and all of them have a discriminator value based on an integral type which points to a mapping in the project (not a FK in database).

We built the mapping like this:

public class ActivityMap : ClassMap<Activity>
    {
        public ActivityMap()
        {
            Table("Activities");
            Id(x => x.Id).Column("ID").GeneratedBy.Guid();
            Map(x => x.ActivityName).Not.Nullable().Length(50);
            HasMany(x => x.ActivityParameters)
                .KeyColumn("ActivityID")
                .AsMap<string>(idx => idx.Column("ParameterName"), elem => elem.Column("ParameterValue"))
                .Not.LazyLoad()
                .Cascade.Delete()
                .Table("ActivityParameters");

            DiscriminateSubClassesOnColumn<int>("ActivityType")
                .SubClass<DummyActivity>(1, c => { });
        }
    }

The generated hbm.xml file is:

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-access="property" auto-import="true" default-cascade="none" default-lazy="true">
  <class xmlns="urn:nhibernate-mapping-2.2" name="***.Activity, ***, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" table="Activities">
    <id name="Id" type="System.Guid, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="ID" />
      <generator class="guid" />
    </id>
    <discriminator column="ActivityType" type="Int32" insert="true" not-null="true" />
    <property name="ActivityName" type="System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="ActivityName" length="50" not-null="true" />
    </property>
    <map cascade="delete" lazy="false" name="ActivityParameters" table="ActivityParameters">
      <key>
        <column name="ActivityID" />
      </key>
      <index type="System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
        <column name="ParameterName" />
      </index>
      <element type="System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
        <column name="ParameterValue" />
      </element>
    </map>
    <subclass name="***.DummyActivity, ***, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" discriminator-value="1" />
  </class>
</hibernate-mapping>

According to my belief, this looks like a valid hbm.xml file, identical in structure with the example given in the official NHibernate reference document, that is

<class name="IPayment" table="PAYMENT">
<id name="Id" type="Int64" column="PAYMENT_ID">
<generator class="native"/>
</id>
<discriminator column="PAYMENT_TYPE" type="String"/>
<property name="Amount" column="AMOUNT"/>
...
<subclass name="CreditCardPayment" discriminator-value="CREDIT">
...
</subclass>
<subclass name="CashPayment" discriminator-value="CASH">
...
</subclass>
<subclass name="ChequePayment" discriminator-value="CHEQUE">
...
</subclass>
</class>

Are we making some mistake in our mapping? Also, can somebody point me the new implementation recommended by Fluent (using SubClass with discriminator column, something like

public class ActivityMap : ClassMap<Activity>
    {
        public ActivityMap()
        {
            Table("Activities");
            Id(x => x.Id).Column("ID").GeneratedBy.Guid();
            Map(x => x.ActivityName).Not.Nullable().Length(50);
            HasMany(x => x.ActivityParameters)
                .KeyColumn("ActivityID")
                .AsMap<string>(idx => idx.Column("ParameterName"), elem => elem.Column("ParameterValue"))
                .Not.LazyLoad()
                .Cascade.Delete()
                .Table("ActivityParameters");

            DiscriminateSubClassesOnColumn<int>("ActivityType");
        }
    }

public class DummyActivityMap : SubClass<DummyActivity>
{
    ///discriminator value here how???
}

?)

The stack trace is

[FormatException: Input string was not in a correct format.]
   System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) +7469351
   System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) +119
   NHibernate.Type.Int32Type.FromStringValue(String xml) +36
   NHibernate.Type.Int32Type.StringToObject(String xml) +10
   NHibernate.Persister.Entity.SingleTableEntityPersister..ctor(PersistentClass persistentClass, ICacheConcurrencyStrategy cache, ISessionFactoryImplementor factory, IMapping mapping) +7824

[MappingException: Could not format discriminator value to SQL string of entity ***.Activity]
   NHibernate.Persister.Entity.SingleTableEntityPersister..ctor(PersistentClass persistentClass, ICacheConcurrencyStrategy cache, ISessionFactoryImplementor factory, IMapping mapping) +8183
   NHibernate.Persister.PersisterFactory.CreateClassPersister(PersistentClass model, ICacheConcurrencyStrategy cache, ISessionFactoryImplementor factory, IMapping cfg) +68
   NHibernate.Impl.SessionFactoryImpl..ctor(Configuration cfg, IMapping mapping, Settings settings, EventListeners listeners) +1468
   NHibernate.Cfg.Configuration.BuildSessionFactory() +87
   FluentNHibernate.Cfg.FluentConfiguration.BuildSessionFactory() in d:\Builds\FluentNH\src\FluentNHibernate\Cfg\FluentConfiguration.cs:93

[FluentConfigurationException: An invalid or incomplete configuration was used while creating a SessionFactory. Check PotentialReasons collection, and InnerException for more detail.

]
   ***.Container.ConfigureNHibernate() in ***.Unity\Container.cs:92
   ***.Container.ConfigureContainer() in ***.Unity\Container.cs:60
   ***.Container.GetInstance() in ***.Unity\Container.cs:45
   ***.Global.CreateContainer() in ***\Global.asax.cs:72
   ***.Global.Application_Start(Object sender, EventArgs e) in ***\Global.asax.cs:44
A: 

I found here that there is a bug related to this situation. I tried in code using (int)1, but it still fails. Still, this guy here shows us an example of table per class hierarchy example. His mapping is similar, his generated hbm file is compatible...

DaeMoohn
I figured it out. Stop using DiscriminateSubClassesOnColumn<int>("ActivityType"), use instead DiscriminateSubClassesOnColumn("ActivityType"). And I also found out how to specify discriminator value in subclass: DiscriminatorValue(1). It's closed, I guess.
DaeMoohn
A: 

Did you figure it out how to use enums instead of integers? With integers it works like you described but with enums it does not. Not even if I cast them to int

E.g. DiscriminatorValue((int)SomeEnum.SomeVaue))

Anonymous
No, i didn't. Please look at http://code.google.com/p/fluent-nhibernate/issues/detail?id=240 , it's an open issue, and I've placed a question there too. Look here too: http://groups.google.com/group/fluent-nhibernate/browse_thread/thread/4d38111df91e72b9 . I use the integer just like an enumeration, maybe I'll adopt it soon.
DaeMoohn
As soon as I'll have a concept working, I'll try with an enum too and I'll give you an answer.
DaeMoohn
+2  A: 

I figured it out in case of enums.

Considering this enum type:

public enum ActivityType
{
    [EnumKey("1")]
    [EnumDescription("ImportFromFile")]
    ImportFromFile,
}

where EnumKey and EnumDescription are (popular) extension methods, I redefine Activity like

public abstract class Activity
    {
        public virtual Guid Id { get; set; }
        public virtual ActivityExecutionResult ExecutionResult { get; private set; }

        public virtual ActivityExecutionStatus ExecutionStatus {get;private set;}

        public abstract ActivityExecutionStatus Execute();

        public virtual string ActivityName { get; private set; }

        public virtual IDictionary<string, string> ActivityParameters { get; private set; }

        public virtual ActivityType ActivityType { get; private set; }
    }

The mapping file looks like this:

public class ActivityMap : ClassMap<Activity>
    {
        public ActivityMap()
        {
            Table("Activities");
            Id(x => x.Id).Column("ID").GeneratedBy.Guid();
            Map(x => x.ActivityName).Not.Nullable().Length(50);
            Map(x => x.ActivityType).CustomType<int>().Column("ActivityType").Not.Nullable();
            HasMany(x => x.ActivityParameters)
                .KeyColumn("ActivityID")
                .AsMap<string>(idx => idx.Column("ParameterName"), elem => elem.Column("ParameterValue"))
                .Not.LazyLoad()
                .Cascade.Delete()
                .Table("ActivityParameters");

            DiscriminateSubClassesOnColumn("ActivityType");
        }
    }

    public class ImportActivityFromFileMap : SubclassMap<ImportActivityFromFile>
    {
        public ImportActivityFromFileMap()
        {
            DiscriminatorValue(ActivityType.ImportFromFile.GetKey());
        }
    }

The generated hbm file looks like:

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-access="property" auto-import="true" default-cascade="none" default-lazy="true">
  <class xmlns="urn:nhibernate-mapping-2.2" name="***.Activity, ***, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" table="Activities">
    <id name="Id" type="System.Guid, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="ID" />
      <generator class="guid" />
    </id>
    <discriminator column="ActivityType" type="String" insert="true" not-null="true" />
    <property name="ActivityName" type="System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="ActivityName" length="50" not-null="true" />
    </property>
    <property name="ActivityType" type="Int32">
      <column name="ActivityType" not-null="true" />
    </property>
    <map cascade="delete" lazy="false" name="ActivityParameters" table="ActivityParameters">
      <key>
        <column name="ActivityID" />
      </key>
      <index type="System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
        <column name="ParameterName" />
      </index>
      <element type="System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
        <column name="ParameterValue" />
      </element>
    </map>
    <subclass name="***.ImportActivityFromFile, ***, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" discriminator-value="1" />
  </class>
</hibernate-mapping>

It works like a charm!

DaeMoohn
Please note that this is no longer considered a bug by the Fluent NHibernate development team. Please read this comment: http://code.google.com/p/fluent-nhibernate/issues/detail?id=240#c10 .
DaeMoohn
A: 

I'll show another implementation that I came up with.

public class SmartEnumMapping<T> : IUserType
    {
        #region IUserType Members

        public object Assemble(object cached, object owner)
        {
            return cached;
        }

        public object DeepCopy(object value)
        {
            return value;
        }

        public object Disassemble(object value)
        {
            return value;
        }

        public int GetHashCode(object x)
        {
            return x.GetHashCode();
        }

        public bool IsMutable
        {
            get { return false; }
        }

        public new bool Equals(object x, object y)
        {
            return object.Equals(x, y);
        }

        public object NullSafeGet(System.Data.IDataReader rs, string[] names, object owner)
        {
            int index0 = rs.GetOrdinal(names[0]);
            if (rs.IsDBNull(index0))
            {
                return null;
            }
            string key = rs.GetString(index0);
            return EnumExtensions.EnumParseKey<T>(key, false, true);
        }

        public void NullSafeSet(System.Data.IDbCommand cmd, object value, int index)
        {
            if (value == null)
            {
                ((IDbDataParameter)cmd.Parameters[index]).Value = DBNull.Value;
            }
            else
            {
                T enumValue = (T)Enum.Parse(typeof(T), value.ToString());
                ((IDbDataParameter)cmd.Parameters[index]).Value = enumValue.GetKey();
            }
        }

        public object Replace(object original, object target, object owner)
        {
            return original;
        }

        public Type ReturnedType
        {
            get { return typeof(T); }
        }

        public global::NHibernate.SqlTypes.SqlType[] SqlTypes
        {
            get { return new SqlType[] { SqlTypeFactory.GetString(4096) }; }
        }

        #endregion
}

With this, the mapping becomes

Map(x => x.ActivityType).CustomType<SmartEnumMapping<ActivityType>>().Column("ActivityType").Not.Nullable();

ActivityType looks like

public enum ActivityType
    {
        [EnumKey("1")]
        [EnumDescription("dada")]
        dad,

        [EnumKey("2")]
        [EnumDescription("da")]
        ImportCalculAtasateSfarsitLuna,

        [EnumKey("3")]
        [EnumDescription("da")]
        das,

        }

With this, in code i can use "ActivityType.das", but when persisting, it is persisted "3". Again, when reading from database, I read "3", but I'll transform that into "ActivityType.das".

I guess this is a more appropriate answer that explains the behaviour mentioned in the preceding answer.

Once again, the EnumKey, EnumDescription, EnumParseKey etc. are stuff that can be easily found on the internet.

DaeMoohn