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> s.Get(typeof(
IPayment), id)
from
Payment p
 from Order o
 join o.payment p
supported
table-per-subclass -many> s.Get(typeof(
IPayment), id)
from
IPayment p
 from Order o
 join o.Payment p
supported
table-per-concrete-class (implicit polymorphism) not supported not supported 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
对象和表结构如下:
NHibernate Inheritance Mapping 继承映射_第1张图片  NHibernate Inheritance Mapping 继承映射_第2张图片
类和配置文件
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()查询时执行的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方式,对象结构不变,表结构如下
NHibernate Inheritance Mapping 继承映射_第3张图片
对于类,只需要把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#的单继承限制了这种设计的作用,同一个父对象,只能派生出一个子对象。

你可能感兴趣的:(NHibernate Inheritance Mapping 继承映射)