NHibernate Inheritance Mapping 继承映射

参考PoEAA,继承的设计模式有:Concrete Table Inheritance (具体表继承) 、Single Table Inheritance (单表继承) 、Class Table Inheritance (类表继承)

Concrete Table Inheritance:
父类为接口或抽象类,不需要存储,每一个子类使用一个独立的表。
这种设计在关系型数据库上处理多态关联、查询时很不方便,例如父类需要关联另外一个类时,所有子类的表都需要加入这个关联字段;如果其它类需要跟父类关联,则对应每一个子类需要添加一个外键 (使用SQL,可以用1个字段去关联所有子类的表,但NHibernate的这种方式不支持,数据库也不能使用外键)
Hibernate中将这种设计分为两种实现方案,一种是table-per-concrete-class with implicit polymorphism (隐式多态) ,另一种table per concrete class with unions。
第一种实现方案下,如果对父类执行一个查询,Hibernate会自动根据父类找出所有的子类,对每个子类的表执行一条查询语句,将返回的结果合并起来生成父类的对象集合。第二种方案下只会执行一条SQL语句,原理是将所有子类UNION起来进行查询。因为将子类UNION起来后可以当作一个表看待,方便的跟其它表关联,因此第二种方案可以很方便的实现多态关联查询。
目前NHibernate只支持第一种方案 (不支持union-subclass ,因此在NHibernate中使用table-per-concrete-class时,在多态关联、查询方面存在限制。
只对那些不需要联合起来进行查询 (多态查询) 的继承体系采用这种设计。
NHibernate对这种方式的限制:
Inheritance strategy Polymorphic many-to-one Polymorphic one-to-one Polymorphic one-to-many Polymorphic many-to-many Polymorphic
Load()/Get()
Polymorphic queries Polymorphic joins Outer join fetching
table-per-class-hierarchy <many-to-one> <one-to-one> <one-to-many> <many-to
-many>
s.Get(typeof(
IPayment), id)
from 
Payment p
 from Order o
 join o.payment p
supported 
table-per-subclass <many-to-one> <one-to-one> <one-to-many> <many-to
-many>
s.Get(typeof(
IPayment), id)
from 
IPayment p
 from Order o
 join o.Payment p
supported 
table-per-concrete-class (implicit polymorphism) <any> not supported not supported <many-to-any> use a query from 
Payment p
not supported  
not supported  

Single Table Inheritance:
继承体系中的所有类都用一个表保存,通过一个字段 (discriminator column) 的值进行区分。Hibernate中叫做table per class hierarchy。这种方式对关系型数据库而言是性能最好的方案,对多态和非多态查询都不错,报表之类的开发不需要大量使用JOIN、UNION。缺点是这个表必须包括继承体系中的所有字段,对非共享字段需要允许为null等。

Class Table Inheritance:
继承体系中的每一个类使用一个表。Hibernate中叫做table-per-subclass,从表的角度看并不是指子类,父类也需要一个表;从对象生命周期等方面看,父类是没有太多意义的,子类才是主角。表结构方面这种方式跟Concrete Table Inheritance有同样的问题,不过它有另外一个特点,就是父对象跟子对象的实体ID值是一样的;父类的表中保存了公共属性,而不是每个子类独立维护;父对象的生命周期跟子对象完全绑定在一起。这些原因使得这种方式能够完全支持各种类型的多态关联、查询,在对象使用层面更方便。

继承,关系型与面向对象最激烈的冲突
这是关系型数据库表现力最弱的地方,却是面向对象最核心的地方。关系型数据库、C#语言特性、Nhibernate三者在这里的制约,给面向对象设计带来最大的限制。
table per concrete class,子类之间的关系最弱,可以基于这一点手工实现多继承特性,但公共属性却是分散的,基类只是一个概念,这一点最烦。
table-per-subclass,把公共属性提取出来放到一个表中,但C#没有多继承的特性,使得这种方式大大逊色。我甚至在怀疑这种设计是否存在悖论,因为父类表中的数据只能被一个子类对象独享,根本没有共享的概念,唯一的好处是NHibernate利用这种表结构比较好的实现了多态查询、关联这个特性,不需要把各个子类特殊的字段揉合在一张表中。手工基于这种表结构设计实现多继承,基本完全用不上NHibernate的继承特性,工作量有点繁琐。
table per class hierarchy,关系型数据库性能问题跟面向对象设计的折中方案,也是现实中最实用的方案,但同样在面向对象方面限制很多。例如如果多层级的继承关系很可能使问题异常复杂化;多继承的问题同样无法突破。

为什么总是提到多继承,因为不少问题确实需要这样处理。
例如企业的物料,原材料跟最终的销售产品属性跟行为都有共性、有差异,但某些物料可能既可以作为原材料,也可以作为产品。物料作为基类,那么这个基类的作用很重要,生命周期应当能跟子类有一定独立性,而三种继承方案里面,基类的作用都是微弱的、受限的。
类似这样的功能,实际中都采用各自独特的结构化设计方式,例如可能将多个物料类型的值拼起来存在一个字段中,或者使用一个字段的位组合表示,对于其它性质的某些功能,可能某些类型就是一些单独的字段表示。

面向对象的特性具备吸引力,ORM工具也总是希望提供良好的映射支持,以比较充分的支持面向对象模型,而关系型数据库的制约与解决复杂问题时设计的技巧性,导致象NHibernate继承特性等,成为一个烫手山芋。

对NHibernate继承方式的选用原则:
1. 不具备充分的理由,尽量不要使用继承映射特性,而利用关联关系,或自己通过模型框架手工实现。
    当你开始考虑组合继承关系实现某些功能时,更是回头的时候。并不是不提倡模型中的继承设计,而是尽量避免使用NHibernate的继承映射特性。自己控制继承体系的存取虽然不会像框架提供的那么自动化,但更有灵活性,更能解决实际问题。
2. 父子对象经常需要联合起来,执行多态查询,需要关注性能问题的(有一定数据量),优先选用Table per class hierarchy。
3. 希望实现类似多继承效果的,使用table per concrete class,手工控制多继承的子对象ID一致,C#没有多继承支持,同样采用手工控制。

继承,贫血的痛处
基于贫血方式使用NHibernate,继承基本上没有获得多少面向对象的优势,而不好的继承设计反而带来关系数据库的性能和使用问题。
因为贫血中的继承基本只是数据模型上的继承,如果要实现行为的继承,难道需要Manager、Impl类也相应的做一套继承体系?那还不如采用充血模型了。对象的业务行为没有继承,就丧失了继承特性80-90%的作用,获得的只是在多态查询、数据实体的使用感觉上一点点安慰性好处。

衡量继承模型带来的优点跟缺点,多跟其它候选方案进行对比是很有必要的。我们的目的是不管局部还是全局视角上,都尽可能简单清晰的原则下考虑、选择每一个设计方案。

手头刚好有个功能,10多个类需要分成两个版本对待:修改状态和发布状态。作用是要保证两种状态数据的隔离,行为上不存在差异,只是存在的业务区域不一样而以。就跟流程引擎重新签核一张有修改的表单一样,在签核完成以前,外部用户看到的只能是修改之前的 (前一次签核过的) 表单资料。
看来这种情况算是贫血里面最适合使用继承的地方了。

table-per-concrete-class
对象和表结构如下:
  
类和配置文件
public  abstract  class BillingDetails
{
     private  string _id;
     private  string _owner;

     public BillingDetails()
    {
    }
     public BillingDetails( string id,  string owner)
    {
         this._id = id;
         this._owner = owner;
    }
     public  virtual  string ID
    {
         get {  return  this._id; }
         set {  this._id = value; }
    }
     public  virtual  string Owner
    {
         get {  return  this._owner; }
         set {  this._owner = value; }
    }
}

public  class CreditCard : BillingDetails
{
     private  string _number;
     private  string _expYear;
     private  string _expMonth;

     public CreditCard()
    {
    }
     public CreditCard( string id,  string owner,  string number,  string month,  string year)
        : base(id, owner)
    {
         this._number = number;
         this._expMonth = month;
         this._expYear = year;
    }
     public  virtual  string Number
    {
         get {  return  this._number; }
         set {  this._number = value; }
    }
     public  virtual  string ExpMonth
    {
         get {  return  this._expMonth; }
         set {  this._expMonth = value; }
    }
     public  virtual  string ExpYear
    {
         get {  return  this._expYear; }
         set {  this._expYear = value; }
    }
}

< class  name ="CreditCard"  table ="CREDIT_CARD" >
     < id  name ="ID" >
         < column  name ="CREDIT_CARD_ID"  sql-type ="VARCHAR2"  length ="36"  not-null ="true" />
         < generator  class ="assigned"   />
     </ id >
     < property  name ="Owner" >
         < column  name ="OWNER"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
     </ property >
     < property  name ="Number" >
         < column  name ="NUMBER"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
     </ property >
     < property  name ="ExpMonth" >
         < column  name ="EXP_MONTH"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
     </ property >
     < property  name ="ExpYear" >
         < column  name ="EXP_YEAR"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
     </ property >
</ class >

public  class BankAccount : BillingDetails
{
     private  string _account;
     private  string _bankName;
     private  string _swift;

     public BankAccount()
    {
    }
     public BankAccount( string id,  string owner,  string account,  string bank,  string swift)
        : base(id, owner)
    {
         this._account = account;
         this._bankName = bank;
         this._swift = swift;
    }
     public  virtual  string Account
    {
         get {  return  this._account; }
         set {  this._account = value; }
    }
     public  virtual  string Swift
    {
         get {  return  this._swift; }
         set {  this._swift = value; }
    }
     public  virtual  string BankName
    {
         get {  return  this._bankName; }
         set {  this._bankName = value; }
    }
}

< class  name ="BankAccount"  table ="BANK_ACCOUNT" >
     < id  name ="ID" >
         < column  name ="BANK_ACCOUNT_ID"  sql-type ="VARCHAR2"  length ="36"  not-null ="true" />
         < generator  class ="assigned"   />
     </ id >
     < property  name ="Owner" >
         < column  name ="OWNER"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
     </ property >
     < property  name ="Account" >
         < column  name ="ACCOUNT"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
     </ property >
     < property  name ="Swift" >
         < column  name ="SWIFT"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
     </ property >
     < property  name ="BankName" >
         < column  name ="BANKNAME"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
     </ property >
</ class >
测试代码:
using (ISession session = TestSetup.GetSession())
{
    CreditCard card1 =  new CreditCard("00000000-0000-0000-0000-000000000001", "Richie", "aaa", "8", "2008");
    CreditCard card2 =  new CreditCard("00000000-0000-0000-0000-000000000002", "Richie", "aab", "8", "2008");
    BankAccount account1 =  new BankAccount("10000000-0000-0000-0000-000000000001", "Richie", "aac", "12", "2008");
    BankAccount account2 =  new BankAccount("10000000-0000-0000-0000-000000000002", "Floyd", "aaa", "12", "2008");
     using (ITransaction tran = session.BeginTransaction())
    {
        session.Save(card1);
        session.Save(card2);
        session.Save(account1);
        session.Save(account2);
        tran.Commit();
    }

    ICriteria criteria = session.CreateCriteria( typeof(BillingDetails));
    criteria.Add(NHibernate.Expression.Expression.Eq("Owner", "Richie"));
    IList<BillingDetails> billings = criteria.List<BillingDetails>();
     foreach (BillingDetails bill  in billings)
        Console.WriteLine(bill.ID);
}
criteria.List<BillingDetails>()查询时执行的SQL语句:
exec sp_executesql N '
    SELECT CREDIT_CARD_ID, OWNER, NUMBER, EXP_MONTH, EXP_YEAR FROM CREDIT_CARD WHERE OWNER = @p0
',
    N ' @p0 nvarchar(6) ', @p0 =N ' Richie '
exec sp_executesql N '     
    SELECT BANK_ACCOUNT_ID, OWNER, ACCOUNT, SWIFT, BANKNAME 
    FROM BANK_ACCOUNT WHERE OWNER = @p0
',
    N ' @p0 nvarchar(6) ', @p0 =N ' Richie '

table-per-subclass
把上面的例子改为table-per-subclass方式,对象结构不变,表结构如下

对于类,只需要把BillingDetails去掉abstract改成具体类,映射文件我们把它放到一个文件中便于查看,把BankAccount.hbm.xml和CreditCard.hbm.xml删除,增加BillingDetails.hbm.xml,内容如下:
< class  name ="BillingDetails"  table ="BILLING_DETAIL" >
     < id  name ="ID" >
         < column  name ="BILLING_ID"  sql-type ="VARCHAR2"  length ="36"  not-null ="true" />
         < generator  class ="assigned"   />
     </ id >
     < property  name ="Owner" >
         < column  name ="OWNER"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
     </ property >
     < joined-subclass  name ="CreditCard"  table ="CREDIT_CARD" >
         < key  column ="CREDIT_CARD_ID"   />
         < property  name ="Number" >
             < column  name ="NUMBER"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
         </ property >
         < property  name ="ExpMonth" >
             < column  name ="EXP_MONTH"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
         </ property >
         < property  name ="ExpYear" >
             < column  name ="EXP_YEAR"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
         </ property >
     </ joined-subclass >
     < joined-subclass  name ="BankAccount"  table ="BANK_ACCOUNT" >
         < key  column ="BANK_ACCOUNT_ID"   />
         < property  name ="Account" >
             < column  name ="ACCOUNT"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
         </ property >
         < property  name ="Swift" >
             < column  name ="SWIFT"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
         </ property >
         < property  name ="BankName" >
             < column  name ="BANKNAME"  sql-type ="VARCHAR2"  length ="40"  not-null ="false" />
         </ property >
     </ joined-subclass >
</ class >
测试代码跟前面的完全一样,但这一次执行有些差异。因为上面的测试代码是新增4个子类对象,NHibernate会自动根据配置关系,在父类BillingDetails对应的表中会新增4条记录。另外查询语句如下,查询结果跟前面的例子一样:
exec sp_executesql N '
SELECT this_.BILLING_ID as BILLING1_14_0_, this_.OWNER as OWNER14_0_, 
    this_1_.NUMBER as NUMBER15_0_, this_1_.EXP_MONTH as EXP3_15_0_, this_1_.EXP_YEAR as EXP4_15_0_, 
    this_2_.ACCOUNT as ACCOUNT16_0_, this_2_.SWIFT as SWIFT16_0_, this_2_.BANKNAME as BANKNAME16_0_, 
    case when this_1_.CREDIT_CARD_ID is not null then 1 
            when this_2_.BANK_ACCOUNT_ID is not null then 2 
            when this_.BILLING_ID is not null then 0 
    end as clazz_0_ 
FROM BILLING_DETAIL this_ 
left outer join CREDIT_CARD this_1_ on this_.BILLING_ID=this_1_.CREDIT_CARD_ID 
left outer join BANK_ACCOUNT this_2_ on this_.BILLING_ID=this_2_.BANK_ACCOUNT_ID 
WHERE this_.OWNER = @p0
',
N ' @p0 nvarchar(6) ', @p0 =N ' Richie '
可以看到,NHibernate在处理多态查询时,自动使用关联执行查询。查询出来的纪录属于哪一个子类,NHibernate使用case when语句用0、1、2标记出来。
这种继承关系的其它一些特性:
1. Get子对象之后,再Get父对象,不会再产生查询SQL。
2. 在子对象上如果只修改了父对象属性,更新时只会对父对象表执行一条更新SQL;如果父子对象的属性都有修改,则更新时对父、子对象的表都会执行更新SQL。
3. 删除子对象时,父对象会被删除;删除父对象,子对象也被删除。他们的生命周期是绑定在一起的。
C#的单继承限制了这种设计的作用,同一个父对象,只能派生出一个子对象。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值