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 | |
|
|
|
s.Get(typeof( IPayment), id) |
from Payment p |
from Order o join o.payment p |
supported |
table-per-subclass | |
|
|
|
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
对象和表结构如下:
类和配置文件
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; }
}
}
{
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 >
< 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; }
}
}
{
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 >
测试代码:
< 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{
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);
}
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 '
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条记录。另外查询语句如下,查询结果跟前面的例子一样:
< 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 >
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标记出来。
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 '
这种继承关系的其它一些特性:
1. Get子对象之后,再Get父对象,不会再产生查询SQL。
2. 在子对象上如果只修改了父对象属性,更新时只会对父对象表执行一条更新SQL;如果父子对象的属性都有修改,则更新时对父、子对象的表都会执行更新SQL。
3. 删除子对象时,父对象会被删除;删除父对象,子对象也被删除。他们的生命周期是绑定在一起的。
C#的单继承限制了这种设计的作用,同一个父对象,只能派生出一个子对象。