代码改变世界

NHibernate学习笔记(2)—关系映射

2009-10-05 22:21  小郝(Kaibo Hao)  阅读(1135)  评论(0编辑  收藏  举报

NHibernate中的集合类型

NHibernate支持/定义的几种类型的集合:

Bag:对象集合,每个元素可以重复。例如{1,2,2,6,0,0},在.Net中相当于IList或者IList<T>实现。

Set:对象集合,每个元素必须唯一。例如{1,2,5,6},在.Net中相当于ISet或者ISet<T>实现,Iesi.Collections.dll程序集提供ISet集合。

List:整数索引对象集合,每个元素可以重复。例如{{1,"YJingLee"},{2,"CnBlogs"},{3,"LiYongJing"}},在.Net中相当于ArraryList或者List<T>实现。

Map:键值对集合。例如{{"YJingLee",5},{"CnBlogs",7},{"LiYongJing",6}},在.Net中相当于HashTable或者IDictionary<Tkey,TValue>实现。

实际上,我们大多数情况下使用Set集合类型,因为这个类型和关系型数据库模型比较接近。


父子关联映射

在NHibernate中,我们可以通过映射文件来关联对象之间的关系。映射文件定义了:

  • 对象之间关系:一对一、一对多、多对一、多对多关系。
  • 在关系中控制级联行为(Cascade behavior):级联更新、级联删除
  • 父子之间的双向导航(bidirectional navigation)

1.父实体映射

父实体(Customer)映射定义了:

  • 集合类型(Bag、Set、List、Map)
  • 在保存、更新、删除操作时的级联行为
  • 关联的控制方向:
    • Inverse="false"(默认):父实体负责维护关联关系
    • Inverse="true":子实体负责维护关联关系
  • 与子实体关联的关系(一对多、多对一、多对多)

这些具体的设置是NHibernate中的难点所在,以后慢慢讨论这些不同设置下的奥秘之处。

这一篇初步建立Customer与Order的一对多关系,修改Customer.hbm.xml映射文件如下:

<?xml version="1.0" encoding="utf-8" ?>
<
hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly
="DomainModel" namespace="DomainModel">

<
class name ="DomainModel.Entities.Customer,DomainModel"
table="Customer">
<
id name="CustomerId" column="CustomerId" type="Int32"
unsaved-value
="0">
<
generator class ="native"></generator>
</
id>
<
property name="Firstname" column ="Firstname" type="string"
length="50" not-null="false"/>
<
property name ="Lastname" column="Lastname" type="string"
length="50" not-null="false"/>
<!--一对多关系:Customer有一个或多个Orders-->
<
set name="Orders" table="`Order`" generic="true" inverse="true">
<
key column="Customer" foreign-key="FK_CustomerOrders"/>
<
one-to-many class="DomainModel.Entities.Order,DomainModel"/>
</
set>

</class>
</
hibernate-mapping>

可以看到,在“父”端映射使用Set元素,标明属性名称、表名、子实体负责维护关联关系。

2.子实体映射

子实体(Order)映射定义的东西就是父实体少了:与父实体关联的(多对一、一对多、多对多) 关系,并用一个指针来导航到父实体。

在“子”端通过many-to-one元素定义与“父”端的关联,从“子”端角度看这种关系模型是多对一关联(实际上是对Customer对象的引用)。下面看看many-to-one元素映射属性:

many-to-one元素定义

看看这些映射属性具体有什么意义:

  • access(默认property):可选field、property、nosetter、ClassName值。NHibernate访问属性的策略。
  • cascade(可选):指明哪些操作会从父对象级联到关联的对象。可选all、save-update、delete、none值。除none之外其它将使指定的操作延伸到关联的(子)对象。
  • class(默认通过反射得到属性类型):关联类的名字。
  • column(默认属性名):列名。
  • fetch(默认select):可选select和join值,select:用单独的查询抓取关联;join:总是用外连接抓取关联。
  • foreign-key:外键名称,使用SchemaExport工具生成的名称。
  • index:......
  • update,insert(默认true):指定对应的字段是否包含在用于UPDATE或INSERT 的SQL语句中。如果二者都是false,则这是一个纯粹的 “外源性(derived)”关联,它的值是通过映射到同一个(或多个)字段的某些其他特性得到或者通过触发器其他程序得到。
  • lazy:可选false和proxy值。是否延迟,不延迟还是使用代理延迟。
  • name:属性名称propertyName。
  • not-found:可选ignore和exception值。找不到忽略或者抛出异常。
  • not-null:可选true和false值。
  • outer-join:可选auto、true、false值。
  • property- ref(可选):指定关联类的一个属性名称,这个属性会和外键相对应。如果没有指定,会使用对方关联类的主键。这个属性通常在遗留的数据库系统使用,可能 有外键指向对方关联表的某个非主键字段(但是应该是一个唯一关键字)的情况下,是非常不好的关系模型。比如说,假设Customer类有唯一的 CustomerId,它并不是主键。这一点在NHibernate源码中有了充分的体验。
  • unique:可选true和false值。控制NHibernate通过SchemaExport工具生成DDL的过程。
  • unique-key(可选):使用DDL为外键字段生成一个唯一约束。

我们来建立“子”端到“父”端的映射,新建Order.hbm.xml文件,编写代码如下:

<?xml version="1.0" encoding="utf-8" ?>
<
hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="DomainModel" namespace="DomainModel">

<
class name="DomainModel.Entities.Order,DomainModel" table="`Order`" >
<
id name="OrderId" column="OrderId" type="Int32" unsaved-value="0">
<
generator class="native" />
</
id>
<
property name="OrderDate" column="OrderDate" type="DateTime"
not-null
="true" />
<!--
多对一关系:Orders属于一个Customer-->
<
many-to-one name="Customer" column="Customer" not-null="true"
class="DomainModel.Entities.Customer,DomainModel"
foreign-key="FK_CustomerOrders"
/>
</
class>
</
hibernate-mapping>

关于如何关联看看上面的属性就一目了然了。


预过滤

使用ICriteria接口的SetResultTransformer(IResultTransformer resultTransformer)方法返回满足特定条件的Customer。上面例子中使用条件查询,观察其生成的SQL语句并没有 distinct,这时可以使用NHibernate.Transform命名空间中的方法或者使用NHibernate提供的 NHibernate.CriteriaUtil.RootEntity、 NHibernate.CriteriaUtil.DistinctRootEntity、 NHibernate.CriteriaUtil.AliasToEntityMap静态方法实现预过滤的作用。那么上面的查询应该修改为:

public IList<Customer> UseCriteriaAPI_GetCustomersWithOrders(DateTime orderDate)
{
return _session.CreateCriteria(typeof(Customer))
.CreateCriteria("Orders")
.Add(Restrictions.Gt("OrderDate", orderDate))
.SetResultTransformer(new NHibernate.Transform.DistinctRootEntityResultTransformer())
//或者.SetResultTransformer(NHibernate.CriteriaUtil.DistinctRootEntity)
.List<Customer>();
}

这个例子从转换结果集的角度实现了我们想要的效果。

 

不太理解的地方

投影

调用SetProjection()方法可以实现应用投影到一个查询中。NHibernate.Criterion.Projections是Projection的实例工厂,Projections提供了非常多的方法,看看下面的截图,下拉列表中的方法是不是很多啊:

Projections方法

现在可以条件查询提供的投影来完成上面同样的目的:

public IList<Customer> UseCriteriaAPI_GetDistinctCustomers(DateTime orderDate)
{
IList<int> ids = _session.CreateCriteria(typeof(Customer))
.SetProjection(Projections.Distinct(Projections.ProjectionList()
.Add(Projections.Property("CustomerId"))
)
)
.CreateCriteria("Orders")
.Add(Restrictions.Gt("OrderDate", orderDate))
.List<int>();

return _session.CreateCriteria(typeof(Customer))
.Add(Restrictions.In("CustomerId", ids.ToArray<int>()))
.List<Customer>();
}

我们可以添加若干的投影到投影列表中,例如这个例子我添加一个CustomerId属性值到投影列表中,这个列表中的所有属性值都设置了 Distinct投影,第一句返回订单时间在orderDate之后所有顾客Distinct的CustomerId,第二句根据返回的 CustomerId查询顾客列表。达到上面的目的。这时发现其生成的SQL语句中有distinct。我们使用投影可以很容易的组合我们需要的各种方 法。


 

对于不太理解的部分我框选了起来。

1. 为什么要查询两次,明明一次SQL查询可以解决的问题却要查询两次。也许是种扩展是为了说明投影而说投影么?还是有什么别的原因?

下面是我执行投影查询时检测到的SQL:

NHibernate: SELECT distinct this_.CustomerId as y0_ FROM Customer this_ inner join [Order] order1_ on this_.CustomerId=order1_.Customer WHERE order1_.OrderDate > @p0;@p0 = 2008-10-1 0:00:00
NHibernate: SELECT this_.CustomerId as CustomerId0_0_, this_.Version as Version0_0_, this_.Firstname as Firstname0_0_, this_.Lastname as Lastname0_0_ FROM Customer this_ WHERE this_.CustomerId in (@p0);@p0 = 1
NHibernate: SELECT orders0_.Customer as Customer1_, orders0_.OrderId as OrderId1_, orders0_.OrderId as OrderId1_0_, orders0_.OrderDate as OrderDate1_0_, orders0_.Customer as Customer1_0_ FROM [Order] orders0_ WHERE orders0_.Customer=@p0;@p0 = 1

其与代码执行的对应关系,据我分析应该如下:














多对多映射关系

1.Order有多个Products


修改Order.cs类代码如下:

namespace DomainModel.Entities
{
public class Order
{
public virtual int OrderId { get; set; }
public virtual DateTime OrderDate { get; set; }
//多对一关系:Orders属于一个Customer
public virtual Customer Customer { get; set; }
//多对多关系:Order有多个Products
public virtual IList<Product> Products { get; set; }
}
}

修改Order.hbm.xml映射文件如下:

<?xml version="1.0" encoding="utf-8" ?>
<
hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="DomainModel" namespace="DomainModel">

<
class name="DomainModel.Entities.Order,DomainModel" table="`Order`" >

<
id name="OrderId" column="OrderId" type="Int32" unsaved-value="0">
<
generator class="native" />
</
id>
<
property name="OrderDate" column="OrderDate" type="DateTime" not-null="true" />
<!--
多对一关系:Orders属于一个Customer-->
<
many-to-one name="Customer" column="Customer" not-null="true"
class="DomainModel.Entities.Customer,DomainModel"
foreign-key="FK_CustomerOrders" />
<!--
多对多关系:Order有多个Products-->
<
bag name="Products" generic="true" table="OrderProduct">
<
key column="`Order`" foreign-key="FK_OrderProducts"/>
<
many-to-many column="Product"
class ="DomainModel.Entities.Product,DomainModel"
foreign-key="FK_ProductOrders"/>
</
bag
>
</
class>
</
hibernate-mapping>

在多对多关系中,其两方都使用Bag集合和many-to-many元素。看看上面各个属性和one-to-many,many-to-one属性差不多。

2.Product属于多个Orders

在项目DomainModel层的Entities文件夹中新建Product.cs类,编写代码如下:

namespace DomainModel.Entities
{
public class Product
{
public virtual int ProductId { get; set; }
public virtual string Name { get; set; }
public virtual float Cost { get; set; }
//多对多关系:Product属于多个Orders
public virtual IList<Order> Orders { get; set; }
}
}

在项目DomainModel层的Mappings文件夹中新建Product.hbm.xml映射文件,编写代码如下:

<?xml version="1.0" encoding="utf-8" ?>
<
hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="DomainModel" namespace="DomainModel">
<
class name="DomainModel.Entities.Product,DomainModel" table="Product">

<
id name="ProductId" column ="ProductId" type="Int32" unsaved-value="0">
<
generator class="native"/>
</
id>
<
property name="Name" column="Name" type="string" not-null="true" length="50"/>
<
property name="Cost" column="Cost" type="float" not-null="true"/>
<!--
多对多关系:Product属于多个Orders-->
<
bag name="Orders" generic="true" table="OrderProduct">
<
key column="Product" foreign-key="FK_ProductOrders"/>
<
many-to-many column="`Order`"
class="DomainModel.Entities.Order,DomainModel"
foreign-key="FK_OrderProducts"/>
</
bag
>
</
class>
</
hibernate-mapping>

使用NHibernate中提供的三种查询方法实现多对多关联查询,查询返回所有订单和产品的顾客列表。

1.原生SQL关联查询

public IList<Customer> UseSQL_GetCustomersWithOrdersHavingProduct(DateTime orderDate)
{
return _session.CreateSQLQuery("select distinct {customer.*} from Customer {customer}" +
" inner join [Order] o on o.Customer={customer}.CustomerId"+
" inner join OrderProduct op on o.OrderId=op.[Order]"+
" inner join Product p on op.Product=p.ProductId where o.OrderDate> :orderDate")
.AddEntity("customer", typeof(Customer))
.SetDateTime("orderDate", orderDate)
.List<Customer>();
}

这里需要使用Join告诉查询如何在表之间关联。无论多么复杂的关系,我们必须在查询语句中指定返回值。这里使用AddEntity设置返回的实体。

2.HQL关联查询

public IList<Customer> UseHQL_GetCustomersWithOrdersHavingProduct(DateTime orderDate)
{
return _session.CreateQuery("select distinct c from Customer c ,"
+ " c.Orders.elements o where o.OrderDate > :orderDate")
.SetDateTime("orderDate", orderDate)
.List<Customer>();
}

因为在映射文件已经定义实体之间一对多、多对多关系,NHibernate通过映射文件知道如何去关联这些实体,我们不需要在查询语句中重复定义。这里使用查询和上一篇使用HQL关联查询语句一样,NHibernate完全可以去关联对象,实现查询订单和产品。

3.Criteria API关联查询

因为实体之间的关联我们在映射文件中已经定义好了。所以我们在查询子对象使用子CreateCriteria语句关联对象之间导航,可以很容易地在 实体之间指定约束。这里第二个CreateCriteria()返回ICriteria的新实例,并指向Orders实体的元素。第三个指向 Products实体的元素。

public IList<Customer> UseCriteriaAPI_GetCustomerswithOrdersHavingProduct()
{
return _session.CreateCriteria(typeof(Customer))
.Add(Restrictions.Eq("Firstname","YJing"))
.CreateCriteria("Orders")
.Add(Restrictions.Gt("OrderDate",new DateTime(2008,10,1)))
.CreateCriteria("Products")
.Add(Restrictions.Eq("Name","Cnblogs"))
.List<Customer>();
}
对于多对多关联的查询对应的SQL语句如下:
------ Test started: Assembly: DALTest.dll ------

NHibernate: SELECT this_.CustomerId as CustomerId0_2_, this_.Version as Version0_2_, this_.Firstname as Firstname0_2_, this_.Lastname as Lastname0_2_, order1_.OrderId as OrderId1_0_, order1_.OrderDate as OrderDate1_0_, order1_.Customer as Customer1_0_, products5_.[Order] as column1_, product2_.ProductId as Product, product2_.ProductId as ProductId3_1_, product2_.Name as Name3_1_, product2_.Cost as Cost3_1_ FROM Customer this_ inner join [Order] order1_ on this_.CustomerId=order1_.Customer inner join OrderProduct products5_ on order1_.OrderId=products5_.[Order] inner join Product product2_ on products5_.Product=product2_.ProductId WHERE this_.Firstname = @p0 and order1_.OrderDate > @p1 and product2_.Name = @p2;@p0 = 'Kaibo', @p1 = 2008-10-1 0:00:00, @p2 = 'Cnblogs'
NHibernate: SELECT orders0_.Customer as Customer1_, orders0_.OrderId as OrderId1_, orders0_.OrderId as OrderId1_0_, orders0_.OrderDate as OrderDate1_0_, orders0_.Customer as Customer1_0_ FROM [Order] orders0_ WHERE orders0_.Customer=@p0;@p0 = 1
NHibernate: SELECT products0_.[Order] as column1_1_, products0_.Product as Product1_, product1_.ProductId as ProductId3_0_, product1_.Name as Name3_0_, product1_.Cost as Cost3_0_ FROM OrderProduct products0_ left outer join Product product1_ on products0_.Product=product1_.ProductId WHERE products0_.[Order]=@p0;@p0 = 1
NHibernate: SELECT products0_.[Order] as column1_1_, products0_.Product as Product1_, product1_.ProductId as ProductId3_0_, product1_.Name as Name3_0_, product1_.Cost as Cost3_0_ FROM OrderProduct products0_ left outer join Product product1_ on products0_.Product=product1_.ProductId WHERE products0_.[Order]=@p0;@p0 = 2

另外还有一个需要注意的地方:对于数据库的关键字如:“Order” 在使用为字段名时,在配置文件中要对该“Column”配置加上特殊转义标记,以示区别,否则会报sql语法错误。
如:
对于SQL数据库关键字的转义<!--多对多关系:Product属于多个Orders-->
    <bag name="Orders" generic="true" table="OrderProduct">
      <key column="Product" foreign-key="FK_ProductOrders"/>
      <many-to-many column="[Order]"
                  class="DomainModel.Entities.Order,DomainModel"
                  foreign-key="FK_OrderProducts"/>
    </bag>
以下是本篇的相关代码:NHibernateSample2.zip
参考资料:

NHibernate之旅(9):探索父子关系(一对多关系)

NHibernate之旅(10):探索父子(一对多)关联查询

NHibernate之旅(11):探索多对多关系及其关联查询

本文部分内容摘录自:

YJingLee's Blog