views:

265

answers:

1

I am trying to create a table-per-hierarchy mapping using NHibernate 2.0.1. I have a base class with properties that exist for each subclass that other classes inherit from. All of these objects are persisted to one table called Messages that contain all of the possible fields for each class. There is a SourceID which is the discriminator and should indicate which Poco to return for each subclass. Here is my current mapping.

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   assembly="NS.Core"
                   namespace="NS.Core.Model">
  <class name="BaseMessage" table="Messages">
    <id name="MessageID" type="Int64">
      <column name="MessageID" />
      <generator class="native" />
    </id>
    <discriminator column="SourceID" type="Int32"/>
    <property name="DateCreated" access="property" column="DateCreated" type="DateTime" not-null="true"/>
    <property name="DatePublished" access="property" column="DatePublished" type="DateTime"/>
    <property name="SourceID" access="property" column="SourceID" type="Int32"/>
    <many-to-one name="User" column="UserID" access="property" cascade="none" lazy="false" fetch="join" outer-join="true" />
    <subclass name="NMessage" discriminator-value="0">
      <property name="Body" access="property" column="Body" type="String"/>
    </subclass>
    <subclass name="BMessage" discriminator-value="1">
      <property name="Title"  access="property" column="Title" type="String"/>
      <property name="Body" access="property" column="Body" type="String"/>
    </subclass>
    <subclass name="CMessage" discriminator-value="2">
      <property name="Url"  access="property" column="Url" type="String"/>
      <property name="Body" access="property" column="Body" type="String"/>
    </subclass>
  </class>
</hibernate-mapping>

I get an error querying saying Could not format discriminator value to SQL string of entity NS.Core.Model.BaseMessage so I put a discriminator value on this class too althout it should never return the base class. That led me to some antlr errors.

Am I taking the wrong approach to this problem? I would like to query the table and get back a list of different POCOs that all inherit from the base class. It would never return the base class itself.

below is the BaseMessage.cs

using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text;

namespace NS.Core.Model
{

    [Serializable]
    [DataContract]
    [KnownType(typeof(User))]
    public class BaseMessage
    {
        #region Private variables
        private long _messageID;
        private DateTime? _dateCreated;
        private DateTime? _datePublished;
        private int _sourceID;
        private User _user = new User();

        #endregion

        #region Properties
        [DataMember]
        public virtual long MessageID
        {
            get { return _messageID; }
            set { this._messageID = value; }
        }
          [DataMember]
        public virtual DateTime? DateCreated
        {
            get { return _dateCreated; }
            set { this._dateCreated = value; }
        }
        [DataMember]
        public virtual DateTime? DatePublished
        {
            get { return _datePublished; }
            set { this._datePublished = value; }
        }
        [DataMember]
        public virtual int SourceID
        {
            get { return _sourceID; }
            set { this._sourceID = value; }
        }
        [DataMember]
        public virtual User User
        {
            get
            {
                if (this._user != null)
                { return this._user; }
                else { return new User(); }
            }
            set { this._user = value; }
        }
        #endregion
    }
}
+3  A: 

The approach is sound. I have done exactly this several times.

You could make it explicit in your code my having BaseMessage's default constructor be protected.

You need to declare a discriminator-value on the base class as well.

I favor string values for discriminators, as this clearer when performing SQL queries or reports. Also, since instances of BaseMessage shoudl not exist, in would use null for its discriminator value.

<class name="BaseMessage" table="Messages" discriminator-value="null">
    <id />
    <discriminator column="SourceID" />
    <subclass name="NMessage" discriminator-value="NMessage">
    </subclass>
    <subclass name="BMessage" discriminator-value="BMessage">
    </subclass>
    <subclass name="CMessage" discriminator-value="CMessage">
    </subclass>
</class>

Also, I see that you have mapped a property to the discriminator column. You should instead have a method that returns something unique to the class - in this case the code.

Note that you cannot change the class of a mapped entity after it has been saved. Not even by changing the discriminator. If you did change it via SQL, your 2nd level cache will still hold a version with the original class.

class BaseMessage
{
    public virtual string MessageType { return null; }
}

class NMessage : BaseMessage
{
    public override string MessageType { return "NMessage"; }
}

Finally, your mapping file is overly verbose as it includes values which are the default. The following attributes and elements can be removed:

  • access="property" - this is the default
  • type="String" - all the types you use can be inferred from your .NET class
  • column="COL" - default is the same as the name
  • similarly for the id column element

All of your Message subclasses have property Body, so move it to the base class mapping. If this field can be longer than your database varchar, it should be a text column and have type="StringCLob" which maps to string in .NET

<class name="BaseMessage" table="Messages" discriminator-value="null">
    <id name="MessageID">
        <generator class="native" />
    </id>
    <discriminator column="SourceID"/>
    <property name="DateCreated" />
    <property name="DatePublished" />
    <many-to-one name="User" column="UserID" cascade="none" lazy="false" fetch="join" outer-join="true" />
    <property name="Body" type="StringCLob" />
    <subclass name="NMessage" discriminator-value="NMessage">
    </subclass>
    <subclass name="BMessage" discriminator-value="BMessage">
        <property name="Title" />
    </subclass>
    <subclass name="CMessage" discriminator-value="CMessage">
        <property name="Url" />
    </subclass>
</class>
Lachlan Roche
What if the database does not hold a SourceID for the BaseMessage, which is never stored in the database?
CountCet
As a mapped class it requires a discriminator value. That such a value will never exist is application level validation logic.
Lachlan Roche
This was a succinct example and the body isn't in every example but your response got this working. Thanks!
CountCet