NHibernate对象关联映射

作者:陈省

 

介绍

       在面向对象开发中,运行时的数据的关联是通过对象之间的引用来实现的。而静态数据的关联是通过表间的外键关联来实现的。而对于一个ORMapping的框架来说,管理对象之间的关联和表间的关系是框架的核心部分。最常见的对象之间的关联是1对多的关联,比如父/子关联,这类关联的数据存储主要是采用主从表的方式来实现,还有一种关联的是多对多的关联,这种关联的数据存储需要将一个n:m关联分解为两个1:n的关联来实现。

 

对象关联映射实例

       接下来,我们将通过一个实例分析来看看NHibernate如何来实现对象关联。假设我们现在要开发一个客户关系管理系统,系统中有三类对象,客户类(Customer)、销售类(Sale)以及订单类(Order),它们之间的关系是客户类同订单类之间的关系是1对多的聚集关系,一个客户可能会下多份订单,订单不能脱离客户而独立存在。而客户类同销售类之间的关系是多对多,根据分析建立的类图示意如下:

其中,Customer类具有CustId属性用于唯一标识Customer对象,CustName对应于客户名称。Order类的OrderId是订单的唯一标识,Content属性描述订单的内容,Amount属性对应于订单的金额,最后一个Index属性描述订单的先后顺序。Sale类同Customer类似,也是有名称和ID属性。

 

类定义的实现

 

首先根据设计创建各个类的定义,首先是客户类的定义:

 

    public class Customer

    {

        public Customer()

        {

            //

            // TODO: 在此处添加构造函数逻辑

            //

        }

 

        public override string ToString()

        {

            return this.CustName;

        }

        private int _CustId;

        //顾客ID

        public int CustId

        {

            get {  return _CustId; }

            set { _CustId = value; }   

        }

        private string _CustName;

        //顾客名称

        public string CustName

        {

            get {  return _CustName; }

            set { _CustName = value; }   

        }

        private IDictionary _Sales;

        public System.Collections.IDictionary Sales

        {

            get { 

                return _Sales;

            }

            set { _Sales = value; }   

        }

        private IList _Orders;

        public IList Orders

        {

            get

            {

                if (_Orders==null)

                    _Orders=new ArrayList();

                return _Orders;

            }

            set

            {

                _Orders=value;

            }

        }

    }

 

注意在客户类的定义中除了基本属性外,我们还定义了类型为IDictionary的Sales属性,建立了客户类同销售类的1对n的单向关联,注意这里用IDictionary类型是因为客户同销售人员的关联中,顺序是无意义。而客户同订单之间的关联采用IList类型定义的Orders属性来实现,是因为IList具有索引属性,可以表示订单的先后顺序。

 

下面是订单类的定义:

 

    public class Order

    {

        public Order()

        {

            //

            // TODO: 在此处添加构造函数逻辑

            //

        }

        private int _OrderId;

        //订单ID

        public int OrderId

        {

            get {  return _OrderId; }

            set { _OrderId = value; }   

        }

   

        private double _Amount;

        //订单金额

        public double Amount

        {

            get {  return _Amount; }

            set { _Amount = value; }    

        }

   

        private string _Content;

        //订单内容

        public string Content

        {

            get {  return _Content; }

            set { _Content = value; }   

        }

 

        private Customer _Cust;

        public NHOrder.Customer Cust

        {

            get {  return _Cust; }

            set { _Cust = value; }   

        }

 

        public int Index

        {

            get {return this.Cust.Orders.IndexOf(this);}

            set {}

        }

    }

要说明的是订单类通过一个Customer类型的Cust属性建立了同客户类的1:1的关联,结合Customer同Order类关联,形成了客户类同订单类的双向关联。另外,Index属性同其它属性不同,属性值只能是Order类在Customer类的Orders列表中的索引(否则NHibernate会无法加载),因此Set方法无须实现。

 

下面是销售人员类的定义:

 

    public class Sale

    {

        public Sale()

        {

            //

            // TODO: 在此处添加构造函数逻辑

            //

        }

        public override string ToString()

        {

            return this.SaleName;

        }

        private int _SaleId;

        //销售ID

        public int SaleId

        {

            get {  return _SaleId; }

            set { _SaleId = value; }   

        }

        private string _SaleName;

        //销售名称

        public string SaleName

        {

            get {  return _SaleName; }

            set { _SaleName = value; }   

        }

        private IDictionary _Customers;

        public System.Collections.IDictionary Customers

        {

            get { 

                if (_Customers==null)

                    _Customers=new Hashtable();

                return _Customers;

            }

            set { _Customers = value; }   

        }

    }

 

销售人员类同客户类类似,也通过IDictionary类型的Cutomers集合属性实现了同客户类之间的多对多的关联。

 

映射关系定义

 

有了类定义,接下来需要定义类同数据库之间的关联映射,先来看客户类的定义:

 

<?xml version="1.0" encoding="utf-8" ?>

 

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0">

    <class name="NHOrder.Customer, NHOrder" table="customers">

        <id name="CustId" column="CustId" type="Int32">

            <generator class="identity" />

        </id>

        <property name="CustName" type="String" length="40"/>

        <set name="Sales" table="customersale" cascade="save-update" inverse="false" lazy="false">

            <key column="CustId" />

            <many-to-many column="SaleId" class="NHOrder.Sale, NHOrder" />

        </set> 

        <list name="Orders" cascade="all" table="orders">

            <key column="CustId" />

            <index column="OrderIndex" type="Int32"/>

            <one-to-many class="NHOrder.Order, NHOrder" />

        </list>        

    </class>

</hibernate-mapping>

定义中的list元素表示客户类同订单类的关联映射为IList类型的属性,元素的Index自元素指定了订单表orders的OrderIndex字段对应于订单的顺序,one-to-many元素则指明同Customer类具有一对多关系的类是NHOrder.Order类,还要注意的是list元素的cascade属性值为all,这表示Customer支持级联更新和删除操作,也就是说当用户删除或修改了客户对象时,客户对象对应的所有订单类也被删除或修改,通过这种客户类同订单类的聚集关系。

 

Xml文件定义中的Set元素标识了客户类同销售类之间的关联是用集合类IDictionary来定义的,many-to-many元素标识了两者多对多的关联。注意Set元素的cascade属性为save-update表示关联只支持级联更新,而不支持级联删除,因为客户同销售人员不是聚集关系,删除一个客户把对应的销售人员也都删除显然是不合理的。Inverse属性则表示两者的关联的反转控制的,也就是说关联关系的保存是由销售类来控制的。Laze属性置为false,表示客户类加载后,同时会将所有关联的销售类也加载进内存,如果为true,则只有当用户真正访问Customer类的Sales属性时才加载相应的数据。

 

相对于客户类的映射定义,订单类和销售类的映射定义相对要简单一些,订单映射内容如下:

 

<?xml version="1.0" encoding="utf-8" ?>

 

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0">

    <class name="NHOrder.Order, NHOrder" table="orders">

        <id name="OrderId" column="OrderId" type="Int32">

            <generator class="identity" />

        </id>

        <property name="Amount" type="Double" />       

        <property name="Content" type="String" length="200"/>

        <many-to-one name="Cust" column="CustId" class="NHOrder.Customer, NHOrder"/>

        <property name="Index" type="Int32" update="true"  insert="true" column="OrderIndex"/>

    </class>

</hibernate-mapping>

 

销售映射定义如下:

<?xml version="1.0" encoding="utf-8" ?>

 

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0">

    <class name="NHOrder.Sale, NHOrder" table="sales">

        <id name="SaleId" column="SaleId" type="Int32">

            <generator class="identity" />

        </id>

        <property name="SaleName" type="String" length="40"/>

        <set name="Customers" table="customersale" cascade="save-update" inverse="true" lazy="false">

            <key column="SaleId" />

            <many-to-many column="CustId" class="NHOrder.Customer, NHOrder" />

        </set> 

    </class>

</hibernate-mapping>

 

 

定义中的订单定义中的many-to-one表示订单类同客户类的关联是多对1的关联。而销售定义中的many-to-many元素定义了销售同客户类的双向关联。

 

表示层定义

 

业务域模型定义好之后,需要设计表示层展现方式,下图是界面设计:

界面中有两个列表,分别对应于客户、订单和销售列表,其中订单列表的展示使用DataGrid来实现,是因为订单类的属性比较多,客户类只需要展示客户名称属性,因此用列表框来展示,销售人员列表则使用CheckListBox来展现,当列表中的项目被打勾时,表示客户同该销售人员有关联关系。三个列表之间具有联动关系,选中某个客户时,订单列表中只显示相应订单,销售列表中只有同该客户关联的销售项才打勾。

 

此外,界面上定义了添加和删除客户的按钮,以及添加客户的订单的按钮。界面上方有两个按钮,一个是数据库Schema生成按钮,一个是查询数据库中数据的按钮,点击后会加载所有的对象数据。

 

应用逻辑实现

 

数据库初始化

 

数据库初始化包括两部分,一个是生成数据库的DbSchema,一个是初始化销售人员列表,前面我们没有根据映射文件来定义数据库生成脚本,这是因为NHibernate提供了SchemaExport类,它可以根据系统内定义的hbm.xml文件,自动生成建库和销毁数据库的代码(注意:由于NHibernate还没有移植Hibernate的所有功能,所以暂时还不能生成数据库迭代演化的修改脚本,因此使用时会破坏原有的数据库数据)。代码示意如下:

 

        private void btnInit_Click(object sender, System.EventArgs e)

        {

            //初始化数据库,注意,这个方法先Drop,然后Create。会破坏

            new SchemaExport(cfg).Create(true, true);

            //创建事先定义好的销售列表

            ISession session=factory.OpenSession();

 

            Sale s=new Sale();

            s.SaleName="销售甲";

            chklstSale.Items.Add(s);

            session.Save(s);

            s=new Sale();

            s.SaleName="销售乙";

            chklstSale.Items.Add(s);

            session.Save(s);

 

            session.Flush();

            session.Close();

        }

 

        ISessionFactory factory;

        Configuration cfg;

        private void OrderForm_Load(object sender, System.EventArgs e)

        {

            try

            {

                cfg = new Configuration();

                cfg.AddAssembly("NHOrder");

 

                factory = cfg.BuildSessionFactory();

            }

            catch(Exception ex)

            {

                MessageBox.Show(ex.Message);

            }

        }

 

要说明的SchemaExport是通过Configuration类获取作为资源嵌入到可执行文件中的hbm.xml文件内容的,资源的加载是通过Configuration的AddAssembly方法实现的。

 

数据查询

 

数据查询会将所有的对象数据加载到内存,代码如下:

        private void btnQuery_Click(object sender, System.EventArgs e)

        {

            this.lbCust.Items.Clear();

            this.chklstSale.Items.Clear();

 

            //获取当前的客户信息

            ISession session=factory.OpenSession();

            //获取当前的销售信息

            IList saleLst=session.CreateCriteria(typeof(Sale)).List();

            foreach (Sale s in saleLst)

                this.chklstSale.Items.Add(s);

            IList custLst = session.CreateCriteria(typeof(Customer)).List();

            session.Close();

            foreach (Customer cust in custLst)

                this.lbCust.Items.Add(cust);

 

            if (lbCust.Items.Count>0)

              this.lbCust.SelectedIndex=0;

        }

在代码的最后,会设定客户列表的当前的SelectedIndex属性设置为0,这会激发客户列表框的SelectedIndexChanged事件,联动的更新订单列表和销售列表的状态,代码如下:

        private Customer GetCurrentCustomer()

        {

            return (Customer)this.lbCust.Items[lbCust.SelectedIndex];

        }

 

        private bool AutoUpdate;

        private void lbCust_SelectedIndexChanged(object sender, System.EventArgs e)

        {

            if (this.lbCust.SelectedIndex==-1)

                return;

 

            Customer cust=this.GetCurrentCustomer();

            this.dgOrders.DataSource=null;

            this.dgOrders.DataSource=cust.Orders;

            //将客户同销售引用关联

            AutoUpdate=true;

            try

            {

                for (int i=0;i<this.chklstSale.Items.Count;i++)

                this.chklstSale.SetItemChecked(i,false);

 

                if (cust.Sales==null)

                    return;

                foreach (Sale s in cust.Sales.Values)

                {

                this.chklstSale.SetItemChecked(this.chklstSale.Items.IndexOf(s),true);

                }

            }

            finally

            {

                AutoUpdate=false;

            }

        }

因为DataGrid的数据绑定支持IList,因此订单数据同DataGrid的绑定直接设定DataSource属性就可以了。

 

添加客户

 

对于一对多的关联关系来说,添加完订单后一定要将Order对象添加到客户类的列表中,同时要设定订单对象的Cust属性为当前的客户对象,否则无法正确保存订单对象及其同客户类的关联,加载Customer.Orders列表对象时NHibernate也就无法根据外键自动地加载数据了。

 

        private void btnAddOrder_Click(object sender, System.EventArgs e)

        {

            NewOrderForm dlg=new NewOrderForm();

            if (dlg.ShowDialog()!=DialogResult.OK)

                return;

            ISession session = factory.OpenSession();

           

            Order order=new Order();

            order.Amount=dlg.Amount;

            order.Content=dlg.Content;

            Customer cust=this.GetCurrentCustomer();

            order.Cust=cust;

            cust.Orders.Insert(cust.Orders.Count, order);

            session.Save(order);

            session.Flush();

            session.Close();

 

            this.lbCust_SelectedIndexChanged(null,null);

        }

还要说明的一点就是,session.save方法并不会将数据马上写入到数据库中,而是当session.flush方法时才会将数据真正写入到数据库中,这样可以实现批量数据的修改,提高性能。

 

删除客户

 

客户数据的删除比较简单,只要调用session.delete方法就可以了,代码如下:

        private void btnDelCust_Click(object sender, System.EventArgs e)

        {

            ISession session=factory.OpenSession();

            ITransaction transaction=session.BeginTransaction();

            try

            {

                Customer cust=this.GetCurrentCustomer();

                session.Delete(cust);

                transaction.Commit();

                this.dgOrders.DataSource=null;

                AutoUpdate=true;

                for (int i=0;i<this.chklstSale.Items.Count;i++)

                    this.chklstSale.SetItemChecked(i,false);

                AutoUpdate=false;

                this.lbCust.Items.RemoveAt(lbCust.SelectedIndex);

 

            }

            catch (Exception ex)

            {

                MessageBox.Show(ex.Message);

                if (!transaction.WasCommitted)

                  transaction.Rollback();

            }

            finally

            {

                session.Close();

            }

        }

 

运行程序后,输入一些数据,点击删除客户按钮后,你会发现数据库中的对应该客户的订单数据也都被删除,实现了级联删除,同时同该客户关联的销售人员则没有被删除。

 

关联客户-销售人员

 

当用户改变销售人员列表中的打勾的状态时,会激发列表框的ItemChecked事件,我们可以在该事件中实现关联功能。同一对多关联的保存不同,对于多对多的关联来说,每增加或删除一个关联,关联两端的对象都需要修改各自的集合属性,同时两端都要进行保存,代码示意如下:

 

         private void chklstSale_ItemCheck(object sender, System.Windows.Forms.ItemCheckEventArgs e)

         {

              if (AutoUpdate)

                   return;

              //将客户同销售进行引用关联

              ISession session=factory.OpenSession();

              Customer cust=this.GetCurrentCustomer();

             Sale s=(Sale)this.chklstSale.Items[e.Index];

              if (e.NewValue==CheckState.Checked)

              {

                   //添加关联

                   cust.Sales.Add(s,s);

                   s.Customers.Add(cust,cust);

                   session.SaveOrUpdate(cust);

                   session.SaveOrUpdate(s);

                   session.Flush();

              }

              else if (e.NewValue==CheckState.Unchecked)

              {

                   cust.Sales.Remove(s);

                   s.Customers.Remove(cust);

                   session.SaveOrUpdate(cust);

                   session.SaveOrUpdate(s);

                   session.Flush();               

              }

            session.Close();

         }

总结

    在本文中,由于篇幅的限制,我们只实现了对象之间最常见的一对多,多对多的关联,事实上NHibernate功能远远不止这些,NHibernate还支持一对一关联,继承关联,组件关联等等,有兴趣的朋友可以自行通过实例和相关文档研究。


本站原创及翻译内容保留版权,欢迎转贴,转贴时请注明转自Delphi深度探索