参考PoEAA,继承的设计模式有:Concrete Table Inheritance (具体表继承) 、Single Table Inheritance (单表继承) 、Class Table Inheritance (类表继承) 。 Concrete 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对这种方式的限制:
Load()/Get()
-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#的单继承限制了这种设计的作用,同一个父对象,只能派生出一个子对象。