views:

113

answers:

1

Hi, my application has the following database structure:

Transactions:
- TransactionID (PK, Identity)
- Type
- TotalAmount

TransactionDetails:
- TransactionDetailID (PK, Identity)
- TransactionID (PK)
- Amount

ProductTransactions:
- TransactionID (PK, FK)
- Discount

ProductTransactionDetails:
- TransactionDetailID (PK, FK)
- ProductID (FK)

I have this mapped using Fluent NHibernate so that ProductTransaction inherits from Transaction and uses a SubclassMap. I did the same for ProductTransactionDetail and TransactionDetail. I also have a property called "Details" which is a list of TransactionDetail on my Transaction entity with the following mapping:

HasMany(x => x.Details)
    .KeyColumn("TransactionID")
    .Inverse()
    .Cascade.All();

I'd like to be able to override this on my ProductTransaction entity. When using virtual and override the compiler complained but new virtual seemed to work. The problem i have is how i map this since the ProductTransactionDetails doesn't have the TransactionID column in the table. It needs to somehow grab it from the parent table but i'm not sure how to do this.

I'd appreciate it if someone could help fix the issue i'm having or let me know if i'm going about things in the wrong way.

Thanks

A: 

Comments are in the code...

Domain Model

public class Product : IEquatable<Product>
{
    protected internal virtual int Id { get; set; }

    public virtual bool Equals(Product other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return other.Id == Id;
    }

    #region Implementation of IEquatable

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != typeof (Product)) return false;
        return Equals((Product) obj);
    }

    public override int GetHashCode()
    {
        return Id;
    }

    public static bool operator ==(Product left, Product right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Product left, Product right)
    {
        return !Equals(left, right);
    }

    #endregion Implementation of IEquatable
}

public class Transaction : IEquatable<Transaction>
{
    private IList<TransactionDetail> details;

    // This is declared protected because it is an implementation
    // detail that does not belong in the public interface of the
    // domain model. It is declared internal so the fluent mapping
    // can see it.
    protected internal virtual int Id { get; set; }

    public virtual double TotalAmount { get; set; }

    // This is declared as a IList even though it is recommended
    // to use ICollection for a Bag because the the Testing Framework
    // passes a HashSet to NHibernate and NHibernate attempts to cast
    // it to a List since it is declared a Bag in the mapping.
    public virtual IList<TransactionDetail> Details
    {
        // I lazily initialize the collection because I do not like
        // testing for nulls all through my code but you may see
        // issues with this if you use Cascade.AllDeleteOrphan in
        // the mapping.
        get { return details ?? (details = new List<TransactionDetail>()); }
        set { details = value; }
    }

    #region Implementation of IEquatable

    // Do not forget to declare this function as virtual or you will
    // get a mapping exception saying that this class is not suitable
    // for proxying.
    public virtual bool Equals(Transaction other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return other.Id == Id;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != typeof(Transaction)) return false;
        return Equals((Transaction)obj);
    }

    public override int GetHashCode()
    {
        return Id;
    }

    public static bool operator ==(Transaction left, Transaction right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Transaction left, Transaction right)
    {
        return !Equals(left, right);
    }

    #endregion Implementation of IEquatable
}

public class TransactionDetail : IEquatable<TransactionDetail>
{
    protected internal virtual int Id { get; set; }
    public virtual double Amount { get; set; }

    #region Implementation of IEquatable

    public virtual bool Equals(TransactionDetail other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return other.Id == Id;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != typeof (TransactionDetail)) return false;
        return Equals((TransactionDetail) obj);
    }

    public override int GetHashCode()
    {
        return Id;
    }

    public static bool operator ==(TransactionDetail left, TransactionDetail right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(TransactionDetail left, TransactionDetail right)
    {
        return !Equals(left, right);
    }

    #endregion Implementation of IEquatable
}

public class ProductTransaction : Transaction, IEquatable<ProductTransaction>
{
    public virtual double Discount { get; set; }

    // This is declared 'new' because C# does not support covariant
    // return types until v4.0. This requires clients to explicitly
    // cast objects of type Transaction to ProductTransaction before
    // invoking Details. Another approach would be to change the
    // property's name (e.g., ProductDetails) but this also requires
    // casting.
    public virtual new IList<ProductTransactionDetail> Details
    {
        get { return base.Details.OfType<ProductTransactionDetail>().ToList(); }
        set { base.Details = null == value ? null : value.Cast<TransactionDetail>().ToList(); }
    }

    #region Implementation of IEquatable

    public virtual bool Equals(ProductTransaction other)
    {
        return base.Equals(other);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        return Equals(obj as ProductTransaction);
    }

    public override int GetHashCode()
    {
        return base.GetHashCode();
    }

    public static bool operator ==(ProductTransaction left, ProductTransaction right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(ProductTransaction left, ProductTransaction right)
    {
        return !Equals(left, right);
    }

    #endregion Implementation of IEquatable
}

public class ProductTransactionDetail : TransactionDetail, IEquatable<ProductTransactionDetail>
{
    public virtual Product Product { get; set; }

    #region Implementation of IEquatable

    public virtual bool Equals(ProductTransactionDetail other)
    {
        return base.Equals(other);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        return Equals(obj as ProductTransactionDetail);
    }

    public override int GetHashCode()
    {
        return base.GetHashCode();
    }

    public static bool operator ==(ProductTransactionDetail left, ProductTransactionDetail right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(ProductTransactionDetail left, ProductTransactionDetail right)
    {
        return !Equals(left, right);
    }

    #endregion Implementation of IEquatable
}

Fluent Mapping

internal sealed class ProductMap : ClassMap<Product>
{
    internal ProductMap()
    {
        Table("Product")
            ;
        LazyLoad()
            ;
        Id(x => x.Id)
            .Column("ProductId")
            .GeneratedBy.Identity()
            ;
    }
}

internal sealed class TransactionMap : ClassMap<Transaction>
{
    internal TransactionMap()
    {
        // The table name is surrounded by back ticks because
        // 'Transaction' is a reserved word in SQL. On SQL Server,
        // this translates to [Transaction].
        Table("`Transaction`")
            ;
        LazyLoad()
            ;
        Id(x => x.Id)
            .Column("TransactionId")
            .GeneratedBy.Identity()
            ;
        Map(x => x.TotalAmount)
            .Column("TotalAmount")
            .Not.Nullable()
            ;
        // You should consider treating TransactionDetail as a value
        // type that cannot exist outside a Transaction. In this case,
        // you should mark the relation as Not.Inverse and save or
        // update the transaction after adding a detail instead of
        // saving the detail independently.
        HasMany(x => x.Details)
            .KeyColumn("TransactionID")
            .Cascade.All()
            .Not.Inverse()
            .AsBag()
            ;
        // You have a Type column in your example, which I took to
        // mean that you wanted to use the Table Per Hierarchy
        // strategy. It this case you need to inform NHibernate
        // which column identifies the subtype.
        DiscriminateSubClassesOnColumn("Type")
            .Not.Nullable()
            ;
    }
}

internal sealed class TransactionDetailMap : ClassMap<TransactionDetail>
{
    internal TransactionDetailMap()
    {
        Table("TransactionDetail")
            ;
        LazyLoad()
            ;
        Id(x => x.Id)
            .Column("TransactionDetailId")
            .GeneratedBy.Identity()
            ;
        Map(x => x.Amount)
            .Column("Amount")
            .Not.Nullable()
            ;
    }
}

internal sealed class ProductTransactionMap : SubclassMap<ProductTransaction>
{
    internal ProductTransactionMap()
    {
        KeyColumn("TransactionId")
            ;
        // I recommend giving the discriminator column an explicit
        // value for a subclass. Otherwise, NHibernate uses the fully
        // qualified name of the class including the namespace. If
        // you later move the class to another namespace or rename
        // the class then you will have to migrate all of the data
        // in your database.
        DiscriminatorValue("TransactionKind#product")
            ;
        Map(x => x.Discount)
            .Column("Discount")
            .Nullable()
            ;
        // Do not map the over-ridden version of
        // the Details property. It is handled
        // by the base class mapping.
    }
}

internal sealed class ProductTransactionDetailMap : SubclassMap<ProductTransactionDetail>
{
    internal ProductTransactionDetailMap()
    {
        // There was no Type column in your example for this table,
        // whcih I took to mean that you wished to use a Table Per
        // Class strategy. In this case, you need to provide the
        // table name even though it is a subclass.
        Table("ProductTransactionDetail")
            ;
        KeyColumn("TransactionDetailId")
            ;
        References(x => x.Product)
            .Column("ProductId")
            .Not.Nullable()
            ;
    }
}

Unit Tests

[TestClass]
public class UnitTest1
{
    private static ISessionFactory sessionFactory;
    private static Configuration configuration;

    [TestMethod]
    public void CanCorrectlyMapTransaction()
    {
        using (var dbsession = OpenDBSession())
        {
            var product = new Product();
            dbsession.Save(product);

            new PersistenceSpecification<Transaction>(dbsession)
                .CheckProperty(t => t.TotalAmount, 100.0)
                .CheckComponentList(
                    t => t.Details,
                    new[] {
                        new TransactionDetail {
                            Amount = 75.0,
                        },
                        new ProductTransactionDetail {
                            Amount = 25.0,
                            Product = product,
                        },
                    }
                )
                .VerifyTheMappings()
                ;
        }
    }

    private static Configuration Configuration
    {
        get
        {
            return configuration ?? (
                configuration = forSQLite().Mappings(
                    m => m.FluentMappings
                        .Conventions.Setup(x => x.Add(AutoImport.Never()))
                        .Add(typeof(ProductMap))
                        .Add(typeof(ProductTransactionMap))
                        .Add(typeof(ProductTransactionDetailMap))
                        .Add(typeof(TransactionMap))
                        .Add(typeof(TransactionDetailMap))
                )
                .BuildConfiguration()
            );
        }
    }

    private static ISessionFactory SessionFactory
    {
        get { return sessionFactory ?? (sessionFactory = Configuration.BuildSessionFactory()); }
    }

    private static ISession OpenDBSession()
    {
        var session = SessionFactory.OpenSession();

        // Ideally, this would be done once on the database
        // session but that does not work when using SQLite as
        // an in-memory database. It works in all other cases.
        new SchemaExport(configuration)
            .Execute(
                true,                 // echo schema to Console
                true,                 // create schema on connection
                false,                // just drop do not create
                session.Connection,   // an active database connection
                null                  // writer for capturing schema
            );

        return session;
    }

    private static FluentConfiguration forSQLite()
    {
        return Fluently.Configure()
            .Database(
                SQLiteConfiguration
                    .Standard
                    .InMemory()
                    .ShowSql()
            );
    }
}
Faron
Hi, thanks for your reply. I tried adding the Details property to my ProductTransaction entity but it still throws the error "Invalid Cast (check your mapping for property type mismatches); setter of ProductTransaction". What other change do i need to make?
nfplee