Richie

Sometimes at night when I look up at the stars, and see the whole sky just laid out there, don't you think I ain't remembering it all. I still got dreams like anybody else, and ever so often, I am thinking about how things might of been. And then, all of a sudden, I'm forty, fifty, sixty years old, you know?

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#的单继承限制了这种设计的作用,同一个父对象,只能派生出一个子对象。

posted on 2007-09-19 18:55 riccc 阅读(...) 评论(...) 编辑 收藏

导航

News