NHibernate - 来自父表的KeyColumn

时间:2010-09-17 08:48:07

标签: nhibernate fluent-nhibernate nhibernate-mapping

我的应用程序具有以下数据库结构:

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

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

ProductTransactions:
- TransactionID (PK, FK)
- Discount

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

我使用Fluent NHibernate进行映射,以便ProductTransaction从Transaction继承并使用SubclassMap。我对ProductTransactionDetail和TransactionDetail做了同样的事情。我还有一个名为“Details”的属性,它是我的Transaction实体上的TransactionDetail列表,其中包含以下映射:

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

我希望能够在ProductTransaction实体上覆盖它。当使用虚拟和覆盖时,编译器抱怨,但新的虚拟似乎工作。我遇到的问题是我如何映射这个,因为ProductTransactionDetails在表中没有TransactionID列。它需要以某种方式从父表中获取它,但我不知道如何做到这一点。

如果有人可以帮我解决我遇到的问题,或者让我知道我是否以错误的方式处理事情,我会很感激。

由于

1 个答案:

答案 0 :(得分:0)

评论在代码中......

域名模型

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

单元测试

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