之前几节的翻译见http://www.cnblogs.com/szp1118/archive/2011/01/17/1937408.html
Bidirectional one-to-many class Relationships
双向的一对多关联关系
在某些情况下,实体之间的双向关联关系是非常有用的.在本节中,我将给你展示怎么样在两个实体类之间建立双向关联关系。
如何去做
1. 创建一个空的类库项目,并命名为ManualRelationships
2. 为该项目添加Iesi.Collections.dll引用,Iesi.Collections.dll文件位于Lib文件夹中
3.添加如下的Order类代码:
{
public virtual Guid Id { get ; protected set ; }
public Order()
{
_items = new HashedSet < OrderItem > ();
}
private ISet < OrderItem > _items;
public virtual IEnumerable < OrderItem > Items
{
get
{
return _items;
}
}
public virtual bool AddItem(OrderItem newItem)
{
if (newItem != null && _items.Add(newItem))
{
newItem.SetOrder( this );
return true ;
}
return false ;
}
public virtual bool RemoveItem(
OrderItem itemToRemove)
{
if (itemToRemove != null &&
_items.Remove(itemToRemove))
{
itemToRemove.SetOrder( null );
return true ;
}
return false ;
}
}
4. 添加一个映射文件Order.hbm.xml,并将其设为嵌入的资源文件
< hibernate-mapping xmlns ="urn:nhibernate-mapping-2.2"
assembly ="ManualRelationships"
namespace ="ManualRelationships" >
< class name ="Order" table ="`Order`" >
< id name ="Id" >
< generator class ="guid.comb" />
</ id >
< set name ="Items"
cascade ="all-delete-orphan"
inverse ="true"
access ="field.camelcase-underscore" >
< key column ="OrderId" />
< one-to-many class ="OrderItem" />
</ set >
</ class >
</ hibernate-mapping >
5. 添加下面的OrderItem类代码
{
public virtual Guid Id { get ; protected set ; }
public virtual Order Order { get ; protected set ; }
public virtual void SetOrder(Order newOrder)
{
var prevOrder = Order;
if (newOrder == prevOrder)
return ;
Order = newOrder;
if (prevOrder != null )
prevOrder.RemoveItem( this );
if (newOrder != null )
newOrder.AddItem( this );
}
}
6. 添加映射文件OrderItem.hbm.xml,代码如下,同样需要设置为嵌入式资源文件:
< hibernate-mapping xmlns ="urn:nhibernate-mapping-2.2"
assembly ="ManualRelationships"
namespace ="ManualRelationships" >
< class name ="OrderItem" >
< id name ="Id" >
< generator class ="guid.comb" />
</ id >
< many-to-one name ="Order" column ="OrderId" />
</ class >
</ hibernate-mapping >
上面的Order类有Items属性,这是个一对多的关联关系,从Order到OrderItem的一对多关联,而在OrderItem类中存在Order属性,这是个多对一的关联关系,从OrderItem到Order的多对一关联,所以说上述两个类之间存在双向关联关系,简称为一对多的双向关联关系。
分析原理
对象关系映射(ORM)是用来解决应用程序中的对象模型和关系数据库的关系模型之间的阻抗不匹配问题的。而这个不匹配问题在实体双向关联的一对多关联关系中尤为明显。在关系模型中,我们是用单一的外键来表示这个双向的关联关系的(只在两个表中的一个表中设置外键就可以了),而在对象模型中,父实体有一个子实体对象的集合属性,而每个子实体又有对父实体的对象引用。
为了解决这个不匹配问题,NHibernate需要忽略双向关联中的其中一边,数据库中的外键可以基于OrderItem实体中的Order实体对象或者是Order实体中Items属性集合中的OrderItem实体对象来构建。但是不需要两边都设置用来作用于外键,我们需要决定到底是哪一边的关联关系来控制这个外键关联,这里我们需要在集合上使用inverse属性(xml映射文件中),默认情况下,inverse的值为false,这样Order对象就控制了这个外键的关系,当我们保存一个带有一个元素的OrderItem的新Order对象时(即该Order对象的Items集合属性中有一个OrderItem元素),NHibernate将会自动生成以下三条SQL语句(第一条语句是保存Order对象时产生的,第二条语句是级联保存OrderItem对象时产生的,第三条语句是建立外键关联关系而产生的)。
INSERT INTO OrderItem (Id) VALUES ( @p0 )
UPDATE OrderItem SET OrderId = @p0 WHERE Id = @p1
当我们把inverse设置为true时,这样Order对象就不会控制这个外键了,而由OrderItem对象来控制,这是推荐的做法,因为这样可以消除额外的UPDATE语句,NHibernate自动生成的SQL语句就变为以下两条了。
INSERT INTO OrderItem (OrderId, Id) VALUES ( @p0 , @p1 )
我们有责任必须保证这个双向关联关系的两端是同步的,是一致的(如果Order类的实例A的属性Items中有一个OrderItem类的实例B,那么这个B对象的属性Order的值必须是A对象,使得这个双向关联是一致的)。在一个规范的类中,我们可以通过在属性的setter访问器中,以及通过为集合属性添加add或者remove方法来自动更新另一端的关联关系。在上面的例子中OrderItem类的属性Order的set访问器被设置为protected的保护级别了,这样就不能对该属性直接赋值,因为直接赋值为导致关联的另一端不同步的情况,我们通过SetOrder方法来为属性Order赋值,在SetOrder方法中,我们添加了额外的代码来使得关联的另一端保持同步,同样地,在Order类,我们通过AddItem和RemoveItem来保持另一端的同步。如果NHibernate去初始化一个对象时,发现存在不同步的情况时会抛出异常。
由于这个原因,推荐的做法是我们必须阻止直接地去操纵这个关联关系的任何一端(意味着我们不能直接给属性赋值,不能直接通过集合进行添加删除操作,我们必须重新定义相应的方法去控制这个同步),就向上面我们定义的AddItem, RemoveItem, 和 SetOrder方法一样。需要注意的是,我们映射的是一个set集合,这就意味着该集合中的元素是无序存放的,并且不允许有重复的元素存在。
更多内容
注意到的是,在Order的映射文件中我们在表名中使用了反撇号,如下代码所示:
在Microsoft SQL Server中,Order是一个关键字,如果我们想把它作为一个标识符的话,上述例子中Order被用作表名标识符,NHibernate需要你在它的前后加上反撇号,这个反撇号告诉NHibernate在生成SQL语句时需要在这个标识符前后加上相应的字符,而具体是加上什么字符需要根据你具体使用的数据库系统来定的,如果是Microsoft SQL Server数据库,那么则会加上中括号。
Mappings enumerations
映射枚举类型
一个不恰当的枚举映射将会导致一些不必要的更新。在本小结中,我将给你展示怎么样去映射一个枚举类型的属性,并将值保存为字符串类型存储至数据库中。
如何去做
1. 创建一个新的类库项目,并命名为MappingEnums
2.添加下面的AccountTypes枚举类型:
{
Consumer,
Business,
Corporate,
NonProfit
}
3. 添加下面的Account类
{
public virtual Guid Id { get ; set ; }
public virtual AccountTypes AcctType { get; set ; }
public virtual string Number { get ; set ; }
public virtual string Name { get ; set ; }
}
4. 为Account类添加一个NHibernate的映射文件,代码如下面所示:
< id name ="Id" >
< generator class ="guid.comb" />
</ id >
< natural-id >
< property name ="Number" not-null ="true" />
</ natural-id >
< property name ="Name" not-null ="true" />
< property name ="AcctType" not-null ="true" />
</ class >
5. 在映射文件中的AcctType属性元素上,添加一个type的属性,值如下:
NHibernate.Type.EnumStringType`1[[MappingEnums.AccountTypes, MappingEnums]], NHibernate
6. 设置该映射文件为嵌入的资源文件。
分析原理
默认情况下,NHibernate映射一个枚举值为一个数字字段,这是根据枚举本身的类型确定的,通常情况下是int类型的。举例来说,如果我们设置AcctType属性的值为AccountTypes.Corporate,那么AcctType在数据库中的字段对应的存储值是2(枚举默认从0开始,Corporate是第三个,所以值是2),这样将会带来不良的后果,一个整型值根据其自身是无法描述该数据的真正业务含义的(意思是说在数据库中你看到这些整型值无法知道其真正的业务含义)。
一个解决办法就是创建一张对应的检索表,表中包含每一个枚举值的描述信息(比如2对应Corporate),但是这样做的话必须要保证应用程序代码和数据库之间的精确的步了,否则将会引起严重的版本问题。简单来说,如果你把代码中的枚举值重新排了一下顺序,而数据库中那个对应表没有相应的改过来,那么将会产生灾难性的后果了。
另一个解决办法是存储枚举值的名称为字符串,例如,如果我们设置AcctType的值为AccountTypes.Corporate,那么AcctType在数据库中的字段值我们存储为Corporate,而不是之前的2了。
通过在映射文件中指定AcctType元素的type属性,我们告诉NHibernate使用一个自定义的类型在.net类型和数据库类型之间进行一个恰当的转换。NHibernate包含了EnumStringType<T>泛型类型,它重写了枚举类型的值和数据库字段值之间的转换方式,默认情况是转换成整型类型存储至数据库,重写了之后是枚举的值的名称作为字符串被存储至数据库。
在映射文件中的type属性值NHibernate.Type.EnumStringType`1[[MappingEnums. AccountTypes, MappingEnums]]中,逗号之后的NHibernate是程序集的完全限定名称,指明了NHibernate.Type.EnumStringType<AccountType>类型在NHibernate程序集中。
补充:个人认为枚举值作为整型类型存储至数据库中并无不妥之处。如果作为名称存储至数据库中,那么也会带来维护上的不便,比如枚举的名称被改变,而值并不改变的情况下,这样的话就需要更新数据库,如果本身存储的就是整型值的话则不需要去更新数据库。
Creating class components
创建组件类
在很多情况下,我们有一组属性值需要被重复使用,这些属性值可能有他们自己的业务逻辑,但是他们在你的应用程序中并没有被定义为一个实体类。他们是值类型的(值类型没有OID)。在本节中,我将给你展示怎么样去处理这些属性值以及它们的业务逻辑,在不创建一个单独的实体类的情况下将它们分隔到一个组件类中去。
如何去做
1. 创建一个新的类库项目,并命名为ComponentExamples
2. 添加一个Address类,代码如下所示(这不是真正的实体类,实体类必须有OID,能够独立存在,并且能区分,而组件类没有OID,是值类型的,无法独立存在,必须依赖于其它的实体类而存在):
public virtual string City { get ; set ; }
public virtual string State { get ; set ; }
public virtual string ZipCode { get ; set ; }
3. 增加一个customer类,代码如下所示(这里组件类Address依赖于customer类而存在):
public virtual Address BillingAddress { get ; set ; }
public virtual Address ShippingAddress { get ; set ; }
4. 为customer类增加相应的映射文件,代码如下,而Address类不需要映射文件,这也是实体类和组件类的不同之处:
< hibernate-mapping xmlns ="urn:nhibernate-mapping-2.2"
assembly ="ComponentExamples"
namespace ="ComponentExamples" >
< class name ="Customer" >
< id name ="Id" >
< generator class ="guid.comb" />
</ id >
< property name ="Name" not-null ="true" />
< component name ="BillingAddress" class ="Address" >
< property name ="Lines" not-null ="true" />
< property name ="City" not-null ="true" />
< property name ="State" not-null ="true" />
< property name ="ZipCode" not-null ="true" />
</ component >
< component name ="ShippingAddress" class ="Address" >
< property name ="Lines" not-null ="true"
column ="ShippingLines" />
< property name ="City" not-null ="true"
column ="ShippingCity" />
< property name ="State" not-null ="true"
column ="ShippingState" />
< property name ="ZipCode" not-null ="true"
column ="ShippingZipCode" />
</ component >
</ class >
</ hibernate-mapping >
分析原理
在这节中,我们使用了Address组件类,而不是去创建一个单独的实体类。我们已经使用该Address组件类来为我们的Customer类定义了billing和shipping这两个地址。它产生的数据库表看起来如下图所示(组件类由于没有OID,所以没有相应的主键,所以在数据库中无法有自己单独的表,从这里就看出,两个地址的字段都被放到了Customer表中去了):
我们的模型看起来如下:
我们在不改变数据库结构的情况下在应用程序代码中充分的体会到了代码复用的所带来的好处,这里是指Address的复用,如果没有定义Address组件类,那么我们在Customer类中需要定义这些相应的属性了。这个Address的这些字段值将会在每次Customer查询的时候都在自动包含进来,它们是被自动加载的,组件类的值是即时加载的,不存在延迟加载的情况的。
补充:组件类对于数据库结构没有什么不同,如果不使用组件类也完全可以,在上面的例子中,如果不使用组件类的话,则Customer的属相会多一些而已,就是把Address的属性放在了Customer类中,这里使用了两次地址,所以这些属性需要在Customer类中定义两次。使用组件类使代码看起来更简洁和优雅,特别是在组件类被使用了很多次的情况下尤为明显。