刘朋:通过前面的讲解,我知道了,Spring.NET提供的“依赖注入”可以降低任意两层(Layer)之间的耦合度。我在写代码的时候又遇到了新的问题,就是在写DAL层的时候,不可避免地会写大量的SQL语句来满足业务上的需求,比如有时候只查询用户表,有时候是用户表和订单表关联查询。于是DAL层充斥着大量的“select字段from table”的SQL语句,这算不算是一种重复代码呢?
MOL:对,只要你可以显而易见地看到重复性的字眼,或者在编码的过程中大量使用了Ctrl+C/Ctrl+V,这些都属于重复代码,当然也违反了DRY原则。
鹏辉:SQL语句都长得差不多啊,但SQL语句又不可能再抽象成什么对象,那么如何避免这种重复代码呢?
MOL:既然SQL语句的出现就必然导致重复代码,那么可以不让它出现,这样不就一劳永逸了吗?
众人哗然……
李冲冲:不写SQL语句,如何获取数据库中的数据呢?
MOL:今天我们要讲的内容,就是让你如何不写SQL语句也能获取数据库中的数据。
当……当……当,主角出场。今天的主角是ORM(Object Relation Mapping,实体关系映射)。
从ORM(Object Relation Mapping,实体关系映射)的字面意思来看,它的目的是要解决实体和关系之间的映射。简单来讲,“实体”就是项目中使用到的实体类,比如前面使用的用户信息类;“关系”就是关系型数据库。
例如,前面创建好的数据库中有一张表是T_CustomInfo_TB,表定义如图3-6所示。
图3-6 表
我们还有一个实体类T_CustomInfo_TB_Model,其定义如代码3-1所示。
【代码3-1】 T_CustomInfo_TB_Model实体的定义:
public class T_CustomInfo_TB_Model { public System.Guid UserInfoID { get; set; } public string LoginUserName { get; set; } public string LoginPassWord { get; set; } public string PasswordQuestion { get; set; } public string PasswordAnswer { get; set; } public string CardNo { get; set; } public Nullable<System.DateTime> CreateDate { get; set; } public string CreateIP { get; set; } public Nullable<System.DateTime> UpdateDate { get; set; } public Nullable<short> DeleteFlag { get; set; } public string ExtraField1 { get; set; } public string ExtraField2 { get; set; } public Nullable<System.DateTime> ExtraField3 { get; set; } public Nullable<decimal> ExtraField4 { get; set; } public Nullable<decimal> ExtraField5 { get; set; } public Nullable<int> ExtOrderField { get; set; } public string CompanyName { get; set; } public string MyAddress { get; set; } public string BackgroundImgPath { get; set; } public string HeadImgPath { get; set; } public virtual ICollection<T_Order_TB_Model> T_Order_TB_Model { get; set; } }
数据库中的表T_CustomInfo_TB就是“关系”,实体定义T_CustomInfo_TB_Model就是“对象”,对于每一个数据库中的字段,都有一个实体的属性与之相对应,这就是“映射”。
当然,映射是非常简单的,我们在使用SqlHelper的时候就已经实现了这种映射。
刘朋:我们在前面已经用过这种写法了啊,好像也没啥神奇的。我直接写个SQL语句,然后把查询结果DataSet映射成实体T_CustomInfo_TB_Model就可以了呀。
MOL:没错,也就是说,你们已经在用ORM了,只不过你们还不知道。需要注意的是,如果只简单地使用数据表到实体间的映射,那么ORM的存在也就没有太大意义了。
在定义T_CustomInfo_TB_Model的最后一行,有一个大家没有见过的定义:
public virtual ICollection<T_Order_TB_Model > T_Order_TB_Model { get;set; }
这行定义描述了用户有一个订单集合的属性。也就是说,一个用户可以有N个订单,N≥0。相应地,在数据库中,T_CustomInfo_TB有一个子表T_Order_TB。
这样一来,数据库中的主子表关系也可以映射到实体中,对我们的编程就更方便了。
刘朋:有啥方便的呢?
MOL:如果用SQL来查询某用户的所有订单,可能会这样写:
Select order.* from T_CustomInfo_TB custom left join T_Order_TB order on custom.userinfoid=order.userinfoid
再进一步,通过SQL语句查询得到了“客户”实体和“订单集合”实体:
var custom=select客户实体; IList<T_Order_TB_Model> orderList=select得到的一个实体集合
那么就需要从订单集合中找到userinfoID等于指定客户的主键的订单,这个查找过程可以使用foreach或for循环,但MOL更喜欢用LINQ,查询表达式如下:
IList<T_Order_TB_Model> 指定用户的订单=(From o in orderList where o.userinfoID= custom.userinfoID select o).ToList();
可以看到,这两种写法都是比较麻烦的,都需要从一堆订单信息中查找所需要的订单。如果使用ORM,就可以很方便地使用“对象.属性”的方式来获取到目标订单。例如,要获取到张三的订单,那么就可以这样写:
T_CustomInfo_TB_Model 张三=查询张三 IList<T_Order_TB_Model> 张三的订单=张三. T_Order_TB_Model.ToList();
这样的写法至少减轻了30%的工作量,而且也使得程序员集中更多的精力去关注业务实体,而不是去重复地编写select代码。
既然ORM这么好用,那么就来看看常见的几种ORM框架吧。下面我们将会认识3种ORM框架,分别是iBATIS.NET、NHibernate和EF(Entity Framework)。
从严格意义上来说iBATIS.NET不能算是一种ORM框架,最明显的就是它必须依赖SQL语句而存在,所以iBATIS.NET充其量只能算是一种半自动化的ORM,它的重点在于映射(Mapping)。
正因为iBATIS.NET需要依赖于SQL语句而存在,所以更适合新手接受,因此我们把它作为第一个框架来讲解。iBATIS.NET是从Java中的iBATIS移植过来的。搭建iBATIS.NET的开发环境分下面几步。
(1)引用DLL
iBATIS.NET用到的DLL如图3-7所示。
图3-7 iBATIS.NET引用的DLL
在本例中,我们引用IBatisNet.Common.dll和IBatisNet.DataMapper.dll。
(2)创建配置文件
配置文件是ORM中最重要的一个环节,不管是现在讲的iBATIS.NET,还是后面讲到的其他的ORM框架。iBATIS.NET用到的配置文件有两个,分别是providers.config和SQLMap.config。
providers.config文件用来描述数据驱动信息,这个配置文件基本上不需要修改,可以从官网上下载最新的配置文件,也可以直接下载本书提供的源代码中的providers.config文件。该文件中包含了大量的provider节点,如图3-8所示。
图3-8 providers.config示例
每一个provider节点都描述了一种访问数据库的驱动。比如图3-8中展开的节点provider描述的是SQLServer的数据访问驱动。
SQLMap.config描述了数据库的连接字符串,如图3-9所示。
图3-9 SQLMap.config示例
把SQLMaps节点展开以后,真是别有洞天。这个节点定义了需要引用的映射文件,看起来是这样的:
<sqlMaps> <sqlMap embedded="Elands.JinCard.Model.Maps.TypeAlias.xml, Elands. JinCard.Model" /> <sqlMap embedded="Elands.JinCard.Model.Maps.Customer.xml, Elands. JinCard.Model" /> <sqlMap embedded="Elands.JinCard.Model.Order.xml, Elands.JinCard. Model" /> </sqlMaps>
上面的代码表示引用了3个XML文件,分别用来定义别名、定义用户实体类及操作、定义订单类及操作。
定义别名的XML文件如下:
<?xml version="1.0" encoding="utf-8" ?> <sqlMap namespace="Account" xmlns="http://ibatis.apache.org/mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <alias> <!--业务对象类型--> <typeAlias alias="T_CustomInfo_TB_Model" type="Elands.JinCard.Model. T_CustomInfo_TB_Model, Elands.JinCard.Model"/> <typeAlias alias="Game" type="Elands.JinCard.Model.T_Order_TB_Model, Elands.JinCard.Model"/> </alias> </sqlMap>
这个XML文件就是定义一个比较好记的别名,然后把这个好记的别名映射到实际的实体全路径中。这个别名映射文件并不是必须的,它的作用就像在写C#代码时使用using来定义别名一样。
(3)创建实体映射及数据操作文件
用户实体类及操作的XML文件如下:
01 <sqlMap namespace="Account" xmlns="http://ibatis.apache.org/mapping" 02 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 03 <resultMaps> 04 <resultMap id="CustomerMap" class="T_CustomInfo_TB_Model"> 05 <result property="UserInfoID" type="string" column="UserInfoID"/> 06 <result property="LoginUserName" type="string" column="Login serName"/> 07 <result property="LoginPassWord" type="string" column="Login assWord"/> 08 <result property="PasswordQuestion" type="string" column= PasswordQuestion"/> 09 <result property="PasswordAnswer" type="string" column= PasswordAnswer"/> 10 <result property="CardNo" type="string" column="CardNo"/> 11 <result property="CreateDate" type="DateTime" column="Create ate"/> 12 <result property="CreateIP" type="string" column="CreateIP"/> 13 <result property="UpdateDate" type="DateTime" column="Update ate"/> 14 <result property="T_Order_TB_Model" type="string" select "getOrderList" /> 15 </resultMap> 16 <resultMap id="OrderMap" class="T_Order_TB_Model"> 17 18 </resultMap> 19 </resultMaps> 20 <statements> 21 <select id="GetSysConfigList" resultMap="ConfigItemResultMap"> 22 <![CDATA[ 23 select UserInfoID, LoginUserName, LoginPassWord, 24 PasswordQuestion,PasswordAnswer,CardNo,CreateDate,CreateIP,UpdateDate rom T_CustomInfo_TB 25 where UserInfoID=#value# 26 ]]> 27 </select> 28 <select id="getOrderList" parameterClass="getOrderList"> 29 <![CDATA[ 30 select * from T_Order_TB where UserInfoID =#value# 31 32 ]]> 33 </select> 34 < insert> 35 <!--这里写的是insert语句--> 36 </insert> 37 < update> 38 <!--这里写的是update语句--> 39 </ update > 40 < delete> 41 <!--这里写的是delete语句--> 42 </ delete > 43 </statements> 44 </sqlMap>
不要被这么一大段代码给搞晕了,其实它只包含两个内容,一个是实体的定义,一个是SQL语句集合。这两个内容又分别对应两个XML节点,分别是resultMaps和statements。
在本节的最开始MOL就说过,iBATIS.NET是依赖于SQL语句存在的。这个中心思想将在当前的XML中表现得淋漓尽致。
在上面的代码中,定义了一个返回T_UserInfo_TB的SQL语句。SQL语句是写在statements/select节点中,statements描述了这个节点下的内容是SQL语句,而select描述了这个节点里的SQL语句是用来执行select操作的。
举一反三,如果需要插入一条数据,那么就需要在statements/insert节点中写insert语句;如果需要更新,那么就需要在statements/update节点中写update语句;如果是删除,就要在statements/delete节点中写delete语句。需要注意的是,iBATIS.NET还支持执行存储过程,如果要执行存储过程,就需要把执行语句写在statements/procedure中。
上面的代码中,从第4行到第15行都是用来定义T_CustomeInfo_TB_Model的实体映射。注意,这里要特别强调一下,这几行代码只是用来定义映射的,而不是用来定义实体的。真正的实体定义需要通过C#代码来定义一个实体类。而这里的XML代码只是把C#中的实体类与数据库中的数据表进行关联,关联的方法就是一个实体属性对应一个数据表字段。
到这里都比较好理解,所谓的映射文件就是把属性和字段对应起来。那么,接着再来看第14行。
第14行描述的是T_Order_TB_Model对象,它是T_CustomInfo_TB_Model的一个子对象。而在数据库中,T_Order_TB是T_CustomInfo_TB的一个子表。第14行的XML代码仅仅描述了这样的主、子表映射的关系。与普通的属性映射定义不同,主、子表映射是没有列(column)概念的,取而代之的是select属性。该属性描述了子表对象需要通过一个select语句来得到。那么通过哪个SQL语句来得到呢?这就需要看select="getOrderList"描述的getOrderList对应的SQL语句了。
看到getOrderList的时候,大家会发现,SQL语句是这样的:
select * from T_Order_TB where UserInfoID =#value#
这里的#value#表示输入参数。SQL语句是比较容易理解的,那么问题来了,#value#这个输入参数是从哪里来的?
根据数据库中的主、子表关系,T_Order_TB中的外键也就是T_CustomInfo_TB的主键将会被作为参数传入上面的SQL语句中。
iBATIS.NET算是一个入门级的、半自动的ORM框架。它的好处是通过配置文件来实现数据库的读写,这样可以很方便地修改配置文件来实现对读写功能的修改,而不用重新编译整个系统。而iBATIS.NET的缺点也是致命的,那就是必须要写大量的SQL语句来实现对数据的读取,需要手动去写大量的映射文件(当然,已经有很多代码生成工具可以帮助我们来做这些事情),也就意味着需要面对很多可能出错的地方。这样看来,iBATIS.NET更像是一个通过面向过程的方式来实现面向对象的目的。
接触过Java的人一定会觉得NHibernate很眼熟,是的,你没有看错,NHibernate就是把Hibernate从Java中移植到了.NET中。到现在为止,NHibernate已经发展到了4.0的版本。如果读者觉得NHibernate的资料太少,那完全可以去看Hibernate的资料,二者的使用方法没有本质的区别。
网络上有大量的教程,这些教程无一例外地会从NHibernate的原理和架构讲起,这样的讲法会把很大一部分不喜欢长篇大论的人拒之门外,并且也不是MOL所推崇的。我们来换一种认识NHibernate的思路,先来做一个NHibernate的Demo(示例),让大家先对NHibernate有一个感性认识,然后再去看看它还有哪些值得我们关注的特性。为了方便,在后面的讲述中将NHibernate简称为NH。
接下来做第一个NHibernate的Demo。
首先到 https://sourceforge.net/projects/nhibernate/files/NHibernate/ 网址下载最新的NHibernate。具体下载过程,MOL就不细说了。下载完成后的文件如图3-10所示。
图3-10 NH包示例
其中Required_Bins中存放的是NH主程序的DLL程序集及配置文件,如图3-11所示。
图3-11 NH主程序DLL及配置文件
nhibernate-configuration.xsd和nhibernate-mapping.xsd分别为NH程序配置和映射配置的xsd文件,把这两个文件复制到Visual Studio安装目录的\Xml\Schemas下,就会有XML的自动提示功能了。
而Configuration_Templates中存放的是数据库相关的配置文件,如图3-12所示。
图3-12 数据库相关的配置文件
可以看到,NH已经支持很多数据库了,如SQL Server、Oracle等。由于我们在本示例中用到的是SQL Server数据库,所以只需要MSSQL.cfg.xml这个配置文件就可以了。
有了这些基本的类库和配置文件,接下来就要做一个NH的Demo了。
完成这个Demo,需要以下3步:
(1)编写实体文件和映射文件。
(2)编写DAO(数据库访问对象)。
(3)编写前台页面。
在这个Demo中,我们还是在操作用户表(T_CustomerInfo_TB),所以再来编写一个T_CustomerInfo_TB的类。代码如下:
//存储用户的详细信息 01 public class T_CustomInfo_TB 02 { 03 /// <summary> 04 /// 登录主键 05 /// </summary> 06 public virtual Guid UserInfoID 07 { 08 get; 09 set; 10 } 11 /// <summary> 12 /// 登录名 13 /// </summary> 14 public virtual string LoginUserName 15 { 16 get; 17 set; 18 } 19 /// <summary> 20 /// 登录密码 21 /// </summary> 22 public virtual string LoginPassWord 23 { 24 get; 25 set; 26 } 27 /// <summary> 28 /// 密码提示问题 29 /// </summary> 30 public virtual string PasswordQuestion 31 { 32 get; 33 set; 34 } 35 /// <summary> 36 /// 密码提示答案 37 /// </summary> 38 public virtual string PasswordAnswer 39 { 40 get; 41 set; 42 } 43 /// <summary> 44 /// CardNo 45 /// </summary> 46 public virtual string CardNo 47 { 48 get; 49 set; 50 } 51 /// <summary> 52 /// 创建时间 53 /// </summary> 54 public virtual DateTime? CreateDate 55 { 56 get; 57 set; 58 } 59 /// <summary> 60 /// 创建IP 61 /// </summary> 62 public virtual string CreateIP 63 { 64 get; 65 set; 66 } 67 /// <summary> 68 /// 更新时间 69 /// </summary> 70 public virtual DateTime? UpdateDate 71 { 72 get; 73 set; 74 } 75 /// <summary> 76 /// 删除标识,1表示未删除,2表示放入回收站,3表示软删除 77 /// </summary> 78 public virtual int? DeleteFlag 79 { 80 get; 81 set; 82 } 83 /// <summary> 84 /// 扩展字段50长度文字 Variable characters (50) 85 /// </summary> 86 public virtual string ExtraField1 87 { 88 get; 89 set; 90 } 91 /// <summary> 92 /// 扩展字段500长度文字 Variable characters (500) 93 /// </summary> 94 public virtual string ExtraField2 95 { 96 get; 97 set; 98 } 99 /// <summary> 100 /// 扩展字段日期时间 Date & Time 101 /// </summary> 102 public virtual DateTime? ExtraField3 103 { 104 get; 105 set; 106 } 107 /// <summary> 108 /// 扩展字段长度为10整数 Number (10) 109 /// </summary> 110 public virtual decimal? ExtraField4 111 { 112 get; 113 set; 114 } 115 /// <summary> 116 /// 扩展字段10位长度2位小数Number (10,2) 117 /// </summary> 118 public virtual decimal? ExtraField5 119 { 120 get; 121 set; 122 } 123 /// <summary> 124 /// 排序字段 125 /// </summary> 126 public virtual int? ExtOrderField 127 { 128 get; 129 set; 130 } 131 /// <summary> 132 /// 发票信息 133 /// </summary> 134 public virtual string CompanyName 135 { 136 get; 137 set; 138 } 139 /// <summary> 140 /// 我的地址 141 /// </summary> 142 public virtual string MyAddress 143 { 144 get; 145 set; 146 } 147 148 }
PS:NH中定义的属性(对应数据库中的字段)一定是用virtual来修饰的。别问为什么,NH中就是这样强制要求的。
与iBATIS不同的是,NH中的类定义是一个真正的class,而iBATIS中的类定义是在配置文件中完成的,相比而言,NH中的类定义更偏向于.NET程序员的编程习惯。
编写完类代码以后,就完成了ORM中的O(对象)。而R(关系)是已经存在的,即数据库中的表T_CustomerInfo_TB。接下来就要完成M(映射)。NH中的映射是一个配置文件,配置文件的命名为XXX.hbm.xml。XXX表示类名,hbm表示hibernate mapping。在本示例中,命名为T_CustomerInfo_TB.hbm.xml。配置文件内容如下:
01 <?xml version="1.0" encoding="utf-8" ?> 02 <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly= "Maticsoft" namespace="Maticsoft"> 03 <class name="Maticsoft.Entity.T_CustomInfo_TB, Maticsoft" table= "T_CustomInfo_TB"> 04 <id name="UserInfoID" column="UserInfoID" type="Guid" unsaved- value="0"> 05 </id> 06 <property name="LoginUserName" column="LoginUserName" type="string" /> 07 <property name="LoginPassWord" column="LoginPassWord" type="string" /> 08 <property name="PasswordQuestion" column="PasswordQuestion" type= "string" /> 09 <property name="PasswordAnswer" column="PasswordAnswer" type= "string" /> 10 <property name="CardNo" column="CardNo" type="string" /> 11 <property name="CreateDate" column="CreateDate" type="DateTime" /> 12 <property name="CreateIP" column="CreateIP" type="string" /> 13 <property name="UpdateDate" column="UpdateDate" type="DateTime" /> 14 <property name="DeleteFlag" column="DeleteFlag" type="int" /> 15 <property name="ExtraField1" column="ExtraField1" type="string" /> 16 <property name="ExtraField2" column="ExtraField2" type="string" /> 17 <property name="ExtraField3" column="ExtraField3" type="DateTime" /> 18 <property name="ExtraField4" column="ExtraField4" type="decimal" /> 19 <property name="ExtraField5" column="ExtraField5" type="decimal" /> 20 <property name="ExtOrderField" column="ExtOrderField" type="int" /> 21 <property name="CompanyName" column="CompanyName" type="string" /> 22 <property name="MyAddress" column="MyAddress" type="string" /> 23 </class> 24 </hibernate-mapping>
这个配置文件中包含两部分内容,粗体内容是Id节点,该节点描述的是数据库中的主键。其他的property节点描述的是非主键字段。对于每一个字段来说,name描述的是实体中的属性名,column描述的是数据库表中的字段名称,type描述的是属性的数据类型。
注意,XXX.hbm.xml的属性“生成操作”需要设置为“嵌入的资源”,如图3-13所示。
图3-13 配置文件设置
到这里为止,第1步就完成了。这一步我们定义了一个类T_CustomerInfo_TB,并且用一个配置文件T_CustomerInfo_TB.hbm.xml来描述实体T_CustomerInfo_TB和数据库表的关联形式。接下来进入第2步。
第2步是通过操作实体来达到操作数据库的目的。顺便回忆一下前面说过的接口。
先定义一个接口,用来描述所有的业务操作,代码如下:
01 interface INHCustomerDAL 02 { 03 /// <summary> 04 /// 将实体保存到数据库中 05 /// </summary> 06 /// <param name="entity"></param> 07 /// <returns></returns> 08 object Save(T_CustomInfo_TB entity); 09 /// <summary> 10 /// 更新实体 11 /// </summary> 12 /// <param name="entity"></param> 13 void Update(T_CustomInfo_TB entity); 14 /// <summary> 15 /// 删除实体 16 /// </summary> 17 /// <param name="entity"></param> 18 void Delete(T_CustomInfo_TB entity); 19 /// <summary> 20 /// 根据主键获取实体 21 /// </summary> 22 /// <param name="id"></param> 23 /// <returns></returns> 24 T_CustomInfo_TB Get(object id); 25 /// <summary> 26 /// 延迟加载,根据主键获取实体 27 /// </summary> 28 /// <param name="id"></param> 29 /// <returns></returns> 30 T_CustomInfo_TB Load(object id); 31 /// <summary> 32 /// 获取所有的实体 33 /// </summary> 34 /// <returns></returns> 35 IList<T_CustomInfo_TB> LoadAll(); 36 }
接下来再定义一个实体操作类,来实现这个接口。在实现之前,需要先引用一下NH的类库,如图3-14所示。
图3-14 引用NH类库
然后完成业务操作类的代码,代码如下:
01 using System; 02 using System.Collections.Generic; 03 using System.Linq; 04 using System.Text; 05 using System.Threading.Tasks; 06 using ELands.JinCard.IDAL; 07 using NHibernate; 08 using NHibernate.Linq; 09 using Elands.JinCard.NH.Model; 10 11 namespace Elands.JinCard.DAL 12 { 13 public class NHCustomerDAL: INHCustomerDAL 14 { 15 /// <summary> 16 /// 获取Session的工厂模式工具 17 /// </summary> 18 private ISessionFactory sessionFactory; 19 /// <summary> 20 /// 构造函数里对工厂进行初始化 21 /// </summary> 22 public NHCustomerDAL() 23 { 24 var cfg = new NHibernate.Cfg.Configuration().Configure ("Config/hibernate.cfg.xml"); 25 sessionFactory = cfg.BuildSessionFactory(); 26 } 27 28 public object Save(T_CustomInfo_TB entity) 29 { 30 using (ISession session = sessionFactory.OpenSession()) 31 { 32 var id = session.Save(entity); 33 session.Flush(); 34 return id; 35 } 36 } 37 38 public void Update(T_CustomInfo_TB entity) 39 { 40 using (ISession session = sessionFactory.OpenSession()) 41 { 42 session.Update(entity); 43 session.Flush(); 44 } 45 } 46 47 public void Delete(T_CustomInfo_TB entity) 48 { 49 using (ISession session = sessionFactory.OpenSession()) 50 { 51 session.Delete(entity); 52 session.Flush(); 53 } 54 } 55 56 public T_CustomInfo_TB Get(object id) 57 { 58 using (ISession session = sessionFactory.OpenSession()) 59 { 60 return session.Get<T_CustomInfo_TB>(id); 61 } 62 } 63 64 public T_CustomInfo_TB Load(object id) 65 { 66 using (ISession session = sessionFactory.OpenSession()) 67 { 68 return session.Load<T_CustomInfo_TB>(id); 69 } 70 } 71 72 public IList<T_CustomInfo_TB> LoadAll() 73 { 74 using (ISession session = sessionFactory.OpenSession()) 75 { 76 return session.Query<T_CustomInfo_TB>().ToList(); 77 } 78 } 79 } 80 }
这样业务操作类就完成了。可以看到,这个业务类里出现了下面一些新的身影。
DAL操作类完成以后,还需要完成BLL业务逻辑层操作类,实现代码如下:
01 public class NHCustomerBLL 02 { 03 private NHCustomerDAL _dal = null; 04 public NHCustomerDAL dal 05 { 06 get 07 { 08 if (_dal == null) 09 { 10 _dal = new NHCustomerDAL(); 11 } 12 return _dal; 13 } 14 } 15 public object Save(T_CustomInfo_TB entity) 16 { 17 return dal.Save(entity); 18 } 19 20 public void Update(T_CustomInfo_TB entity) 21 { 22 dal.Update(entity); 23 } 24 25 public void Delete(T_CustomInfo_TB entity) 26 { 27 dal.Delete(entity); 28 } 29 30 public T_CustomInfo_TB Get(object id) 31 { 32 return dal.Get(id); 33 } 34 35 public T_CustomInfo_TB Load(object id) 36 { 37 return dal.Load(id); 38 } 39 40 public IList<T_CustomInfo_TB> LoadAll() 41 { 42 return dal.LoadAll(); 43 } 44 }
业务操作类完成以后,就可以用前台来展示了,即进入第3步。这一步,我们将做一个前台页面来展示查询得到的数据。
创建MVC项目的过程这里跳过,如果忘记的读者,请回到第2章中重新复习一下。
创建好MVC项目以后,需要增加一个NHCustomController控制器,为控制器增加一个业务操作类,并修改Index()方法,代码如下:
01 public class NHCustomController : Controller 02 { 03 private NHCustomerBLL _bll = null; 04 public NHCustomerBLL bll 05 { 06 get 07 { 08 if (_bll == null) 09 { 10 _bll = new NHCustomerBLL(); 11 } 12 return _bll; 13 } 14 } 15 // GET: NHCustom 16 public ActionResult Index() 17 { 18 IList<T_CustomInfo_TB> customList = bll.LoadAll(); 19 return View(customList); 20 } 21 }
为Index()方法增加一个视图页面,右击index()方法,然后在弹出的快捷菜单中选择“添加视图”命令,在弹出的对话框中修改“模板”为List,“模型类”为我们创建好的T_CustomInfo_TB,然后单击“添加”按钮,如图3-15所示。
图3-15 “添加视图”对话框
到这里并没有完。大家是否还记得我们在定义实体操作类NHCustomerDAL的时候,MOL说过,需要在构造函数中将sessionFactory这个工厂实例化,实例化的时候,需要读取配置文件。代码如下:
public NHCustomerDAL() { var cfg = new NHibernate.Cfg.Configuration().Configure("Config/hibernate. cfg.xml"); sessionFactory = cfg.BuildSessionFactory(); }
上面代码中描述的是NH了一个配置文件,该配置文件放在Config文件夹下,配置文件为hibernate.cfg.xml。前方高能请注意!我见过很多程序员在DAL层的项目中建了一个Config,然后高高兴兴地在这个文件夹下放了一个配置文件,最后在运行的时候发现网站报错,错误信息是找不到配置文件。配置文件一定要放在运行项目的目录下,如当前示例中,运行项目是Elands.JinCard.Protal.MVC,那么就需要在MVC项目下创建Config文件夹,在Config文件夹下存放hibernate.cfg.xml配置文件,并且这个配置文件的属性一定要设置成“始终复制”,如图3-16所示。
图3-16 配置文件需要设置为“始终复制”
做完这一切以后,直接按F5键运行程序,就可以看到运行结果了。
刘朋:什么呀,我就想查个数据,NH这么难,这样真的好吗?
鹏辉:是呀,光是搭建NH的环境就好麻烦,我有点望而却步了。
冲冲:难道是有什么吾辈未曾察觉的优点?
MOL:我的目的就是把你们绕晕了,用来显示我是多么“高大上”。
刘朋、鹏辉、冲冲:嘁~~
MOL:好了,言归正转。NH的配置非常麻烦,这是无可争议的事实。但是那些学Java的人可是对这么复杂的配置乐此不疲哦。NH环境配置好以后,就可以不用写SQL语句来操作数据库了,这样不是达到你们偷懒的目的了吗?
冲冲:这样是可以不用写SQL语句了,但是在复杂的配置和繁琐的SQL语句两者中非要做一个选择的话,我还是会选SQL语句,至少我们对SQL语句非常熟悉啊。
MOL:非常好。我就喜欢你们这种懒出境界的人生态度。既然选择了懒,那我们就一懒到底。接下来要讲的ORM,一定非常对你们的胃口。
接下来我们要讲的ORM叫Entity Framework,简称为EF。EF是微软基于ADO.NET提供的一种ORM组件,它的特点就是一个字“简单”。
冲冲:简单貌似是两个字啊。
MOL:好吧,那就两个字吧。
如果你用过NH,那你一定会觉得EF是如此的简单,因为它不需要你自己去写类、映射文件、驱动文件等,所有的这一切,EF都已经帮你写好了。下面通过一个Demo来看看EF是如何的简单。
写一个EF项目需要下面4步:
(1)创建实体层。
(2)创建数据操作层。
(3)创建业务逻辑层。
(4)创建视图层。
接下来我们就按照上面的步骤来做这个EF的Demo。
创建一个项目,名为Elands.JinCard.EF.Model,为这个项目添加一个“ADO.NET实体数据模型”,如图3-17所示。
图3-17 添加实体模型第1步
在弹出的对话框中选择“来自数据库的EF设计器”选项,然后单击“下一步”按钮,如图3-18所示。
图3-18 添加实体模型第2步
在弹出的对话框中单击“新建连接”按钮,如图3-19所示。
图3-19 添加实体模型第3步
在弹出的对话框中按下面的步骤来输入,如图3-20所示。
图3-20 添加实体模型第4步
(1)输入服务器名称,本示例中,数据库使用本地的JinCardDB,所以在“服务器名”里输入“.”即可。
(2)选择“使用SQL Server身份验证”单选按钮。
(3)输入数据库的登录用户名和密码。
(4)在“选择或输入数据库名称”中选择JinCardDB选项。
(5)单击“测试连接”按钮,提示“测试连接成功”。
(6)单击“确定”按钮继续。
设置完成以后,就得到了一个新的数据库连接,在对话框中选择“是,在连接字符串中包括敏感数据”单选按钮,单击“下一步”按钮,如图3-21所示。
图3-21 添加实体模型第5步
在弹出的对话框中选择EF版本,保持默认即可,继续单击“下一步”按钮,如图3-22所示。
图3-22 添加实体模型第6步
在弹出的对话框中选择要生成对象的数据库表或视图,在本例中只需要生成数据库表就可以了,如图3-23所示。
图3-23 添加实体模型第7步
最后单击“完成”按钮即可。
完成以后,在Visual Studio左边的窗口中显示的是类图,也就是数据库表映射得到的类,在右边的解决方案管理器中,也以树的形式展示生成了哪些类,如图3-24所示。
图3-24 添加实体模型第8步
到这里为止,就完成了步骤1的内容。在这一步里,我们只是点了几下鼠标,就完成了实体层的创建。相比NH的实体层建立,是不是非常简单?
如果说EF里有哪一步是比较难的话,那么步骤2可以算是最难的,怎么个难法呢?嘿嘿,千万不要被“难”给吓倒了,这里所谓的难,也只是相对难而已。
和NH一样,我们还是来创建一个接口层,用来规范操作方法。新建一个项目Elands.JinCard.EF.IDAL,在项目里添加一个IEFCustomerDAL接口,用来描述操作T_CustomInfo_TB对象的方法。接口代码如下:
01 public interface IEFCustomerDAL 02 { 03 /// <summary> 04 /// 是否存在该记录 05 /// </summary> 06 bool Exists(Guid EntityID); 07 /// <summary> 08 /// 删除数据 09 /// </summary> 10 bool Delete(Guid EntityID); 11 /// <summary> 12 /// Add Function 13 /// </summary> 14 /// <param name="input"></param> 15 /// <returns></returns> 16 T_CustomInfo_TB Add(T_CustomInfo_TB input); 17 /// <summary> 18 /// 获取所有的实体 19 /// </summary> 20 /// <returns></returns> 21 IList<T_CustomInfo_TB> GetListAll(); 22 /// <summary> 23 /// 更新单个实体 24 /// </summary> 25 /// <param name="model"></param> 26 /// <returns></returns> 27 bool Update(T_CustomInfo_TB model); 28 /// <summary> 29 /// 根据ID查询实体 30 /// </summary> 31 /// <typeparam name="T"></typeparam> 32 /// <typeparam name="TType"></typeparam> 33 /// <param name="id"></param> 34 /// <returns></returns> 35 T_CustomInfo_TB GetModel(Guid id); 36 }
再创建一个项目Elands.JinCard.EF.DAL,用来实现接口层所定义的方法。在这个项目中添加一个EFCustomerDAL类,用来实现IEFCustomerDAL接口。代码如下:
01 using System; 02 using System.Collections.Generic; 03 using System.Linq; 04 using Elands.JinCard.EF.IDAL; 05 using Elands.JinCard.EF.Model; 06 using System.Data.Entity; 07 using System.Data; 08 09 namespace Elands.JinCard.EF.DAL 10 { 11 public class EFCustomerDAL : IEFCustomerDAL 12 { 13 public DbContext context = new JinCardDBEntities(); 14 public T_CustomInfo_TB Add(T_CustomInfo_TB input) 15 { 16 context.Configuration.ValidateOnSaveEnabled = false; 17 context.Entry<T_CustomInfo_TB>(input).State = System.Data. Entity.EntityState.Added; 18 context.SaveChanges(); 19 context.Configuration.ValidateOnSaveEnabled = true; 20 return input; 21 } 22 23 public bool Delete(Guid EntityID) 24 { 25 T_CustomInfo_TB delModel = GetModel(EntityID); 26 context.Entry<T_CustomInfo_TB>(delModel).State = System. Data.Entity.EntityState.Deleted; 27 return context.SaveChanges() > 0; 28 } 29 30 public bool Exists(Guid EntityID) 31 { 32 T_CustomInfo_TB model = GetModel(EntityID); 33 return model != null; 34 } 35 36 public IList<T_CustomInfo_TB> GetListAll() 37 { 38 return context.Set<T_CustomInfo_TB>().ToList(); 39 } 40 41 public T_CustomInfo_TB GetModel(Guid id) 42 { 43 return context.Set<T_CustomInfo_TB>().Find(id); 44 } 45 46 public bool Update(T_CustomInfo_TB model) 47 { 48 context.Entry<T_CustomInfo_TB>(model).State = System.Data. Entity.EntityState.Modified; 49 return context.SaveChanges() > 0; 50 } 51 }}
在上面的代码中,有一个非常重要的对象需要着重讲解一下。在第13行中定义了一个DbContext对象,这个对象相当于NH中的ISession对象,是数据库和实体类之间的桥梁,通常将DbContext对象叫做“上下文”。DbContext是一个父类,实例化这个父类的时候,用的是连接JinCardDB的实体类。也就是说,实例化后的DbContext类就可以操作JinCardDB数据库中的数据了。
DbContext对象提供了很多方法用来操作数据。常见的方法如下。
只需要这两个方法就足以应付绝大多数的需求了,是不是很简单?
这一步是最简单的,只需要创建BLL类并实现即可。创建一个业务逻辑层项目Elands.JinCard.EF.BLL,添加一个EFCustomerBLL,这个BLL类是用来实现业务逻辑的,因为本示例比较简单,没有复杂逻辑,所以在BLL类中只是简单地调用DAL返回数据。实现代码如下:
01 using System; 02 using System.Collections.Generic; 03 using Elands.JinCard.EF.Model; 04 using Elands.JinCard.EF.IDAL; 05 using Elands.JinCard.EF.DAL; 06 07 namespace Elands.JinCard.EF.BLL 08 { 09 public class EFCustomerBLL 10 { 11 12 IEFCustomerDAL dal = new EFCustomerDAL(); 13 public T_CustomInfo_TB Add(T_CustomInfo_TB input) 14 { 15 return dal.Add(input); 16 } 17 18 public bool Delete(Guid EntityID) 19 { 20 return dal.Delete(EntityID); 21 } 22 23 public bool Exists(Guid EntityID) 24 { 25 return dal.Exists(EntityID); 26 } 27 28 public IList<T_CustomInfo_TB> GetListAll() 29 { 30 return dal.GetListAll(); 31 } 32 33 public T_CustomInfo_TB GetModel(Guid id) 34 { 35 return dal.GetModel(id); 36 } 37 38 public bool Update(T_CustomInfo_TB model) 39 { 40 return dal.Update(model); 41 } 42 } 43 }
视图层主要是用来与客户交互的,在本示例中,我们只简单地把数据库中的数据展示出来。因为新建控制器和视图的过程在前面都已经讲过了,所以此处略去不讲。
创建好的控制器如下:
01 public class EFCustomerController : Controller 02 { 03 private EFCustomerBLL bll = new EFCustomerBLL(); 04 // GET: EFCustomer 05 public ActionResult Index() 06 { 07 IList<T_CustomInfo_TB> cuList = bll.GetListAll(); 08 return View(cuList); 09 } 10 }
最后,需要在web.config中加上关于数据库连接的描述,代码如下:
<connectionStrings> <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\ MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-Elands.JinCard.Protal.EF.MVC-20161010124709.mdf;Initial Catalog=aspnet-Elands.JinCard.Protal. EF.MVC-20161010124709;Integrated Security=True" providerName="System.Data. SqlClient" /> <add name="JinCardDBEntities" connectionString="metadata=res://*/EFModel. csdl|res://*/EFModel.ssdl|res://*/EFModel.msl;provider=System.Data.SqlClient;provider connection string="data source=.;initial catalog= JinCardDB;persist security info=True;user id=sa;password=000;Multiple ActiveResultSets=True;App=EntityFramework"" providerName="System.Data. EntityClient" /> </connectionStrings>
黑体部分是新加的代码,这段代码千万不要自己写。找到实体层中的app.config配置文件,把app.config中关于数据库连接的描述粘贴到当前web.config中即可。
运行程序,perfect!运行结果如图3-25所示。
图3-25 EF的Demo运行结果
这个运行结果虽然“丑陋”,但它是我们对EF的理解迈出的重要一步,此处应该有掌声。
冲冲:这样实现起来确实是更面向对象了。
MOL:说重点!
冲冲:使用更方便了。
MOL:说重点!!
冲冲:好吧,更简单了。
MOL:嗯。(心满意足状)
鹏辉:这样确实很简单了。我们只需要操作对象就可以实现数据的增、删、改、查。但是我还有个问题。
MOL:(一脸惊恐状)什么问题?
鹏辉:针对第一个数据库表,我都要去实现对它的操作,但这些操作基本上都是一样的,如果我针对每个表都去写类似的代码,那这样不就违反“干燥”(DRY)原则了吗?
刘朋:哎哟喂,说得还真有那么点道理。
MOL:岂只是有点道理,简直就是真理嘛。接下来我们就来看看如何对一个“湿”的设计进行“干燥”处理。
正如鹏辉所说,几乎所有的实体类的CRUD(增、删、改、查)的代码都是类似的,那我们就想办法干掉这些重复的代码。
.NET中有一种神奇的方法叫泛型方法。这种方法可以定义未知类型的输入或输出参数。也就是说,实体类可以被当做参数传递到函数中,最终返回指定的实体类。
先来看一下根据主键来获取非泛型函数是这样的:
public T_CustomerInfo_TB GetModel(GUID id) { return context.Set<T_CustomerInfo_TB>().Find(id); }
而泛型函数是这样的:
public T GetModel(GUID id) { return context.Set<T>().Find(id); }
泛型函数中的T就代表了一种类型,具体是什么类型,调用的时候才知道。调用泛型函数的代码是这样的:
T_CustomerInfo_TB customer=DAL.GetModel<T_CustomerInfo_TB>(guid);
同样的,其他函数也是一样的道理,这里就不一一说明了。在接下来的代码中,也只以“根据主键获取实体”这一个函数来举例。
这些泛型方法应该放在哪里合适呢?既然我们把操作数据的函数都抽象成泛型了,也就意味着我们必须新建一个类来存放这些泛型函数。这个类必须是泛型的,而且要对T类型进行描述。代码如下:
public class BaseDal<T > where T : class , new() { public DbContext context = EFDbContextFactory.GetCurrentDbContext(); public T GetModel(GUID id) { return context.Set<T>().Find(id); } //其他函数 }
定义一个BaseDal类,这是一个泛型类,类定义后面的where T:class,new()函数用来限制T是一种可以被new出来的类型。因为context.Set<T>要求T必须是一个可实例化的类,所以需要加这个限制。
刘朋:我依稀记得DAL是应该继承IDAL的吧,那么IDAL是不是也得变成泛型接口啊。
MOL:是这样的。这就是传说中的“鱼找鱼,虾找虾,乌龟找王八”。泛型的类也需要实现一个泛型的接口。泛型接口的代码如下:
01 public interface IBaseDal<T> where T : class, new() 02 { 03 /// <summary> 04 /// 是否存在该记录 05 /// </summary> 06 bool Exists(Guid EntityID); 07 /// <summary> 08 /// 删除数据 09 /// </summary> 10 bool Delete(Guid EntityID); 11 /// <summary> 12 /// Add Function 13 /// </summary> 14 /// <param name="input"></param> 15 /// <returns></returns> 16 T Add(T input); 17 /// <summary> 18 /// 获取所有的实体 19 /// </summary> 20 /// <returns></returns> 21 IList<T> GetListAll(); 22 /// <summary> 23 /// 更新单个实体 24 /// </summary> 25 /// <param name="model"></param> 26 /// <returns></returns> 27 bool Update(T model); 28 /// <summary> 29 /// 根据ID查询实体 30 /// </summary> 31 /// <typeparam name="T"></typeparam> 32 /// <typeparam name="TType"></typeparam> 33 /// <param name="id"></param> 34 /// <returns></returns> 35 T GetModel(Guid id); 36 }
定义好泛型接口以后,就需要让BaseDal来继续IBaseDal这个接口。修改BaseDal的定义代码如下:
using System; using System.Collections.Generic; using System.Linq; using Elands.JinCard.EF.IDAL; using System.Data.Entity; using Elands.JinCard.EF.Model; namespace Elands.JinCard.EF.DAL { public class BaseDal<T> : IBaseDal<T> where T : class, new() { public DbContext context = new JinCardDBEntities(); public T Add(T input) { context.Configuration.ValidateOnSaveEnabled = false; context.Entry<T>(input).State = EntityState.Added; context.SaveChanges(); context.Configuration.ValidateOnSaveEnabled = true; return input; } public bool Delete(Guid EntityID) { T delModel = GetModel(EntityID); context.Entry<T>(delModel).State = EntityState.Deleted; return context.SaveChanges() > 0; } public bool Exists(Guid EntityID) { T model = GetModel(EntityID); return model != null; } public IList<T> GetListAll() { return context.Set<T>().ToList(); } public T GetModel(Guid id) { return context.Set<T>().Find(id); } public bool Update(T model) { context.Entry<T>(model).State = EntityState.Modified; return context.SaveChanges() > 0; } } }
这样就完成了一个实体操作类的“基类”,所有的业务实体类都可以通过继承BaseDal来实现具体的操作方法,也就避免了对每一个实体类都去写相同的代码来操作数据库。以T_CustomerInfo_TB这个实体类为例,继承基类的方法如下:
public partial class T_CustomerInfo_TBDAL : BaseDal<T_CustomerInfo_TB> { }
就这么短短的一句话,就可以实现关于T_CustomerInfo_TB的CRUD操作。如果需要实现对订单实体的操作,那么只需要把T_CustomerInfo_TB换成是T_Order_TB就可以了。
同样的,对于接口来说,继承基类接口的方法为:
public interface IT_CustomerInfo_TBDAL :IDALBase<T_CustomerInfo_TB> { }
完成接口以后,记得要给对应的DAL加上实现约束,也就是让DAL实现指定的IDAL。在本示例中,就是让T_CustomerInfo_TBDAL实现IT_CustomerInfo_TBDAL。修改T_CustomerInfo_TB的定义方法如下:
public partial class T_CustomerInfo_TBDAL : BaseDal<TianG.Model.T_ Customer Info_TB>, IT_CustomerInfo_TBDAL { }
注意:当子类同时继承父类和接口的时候,父类要写在前面,后面写接口,父类和接口之间用逗号分隔。
这样就完成了“干燥”(DRY)的第一步,将业务类的操作抽象到基类中。完成后的代码结构如图3-26所示。
图3-26 抽象基类后的代码结构
鹏辉:如果我要新增订单类的DAL的话,就先新增一个订单的操作接口来实现IBaseDal,再创建一个订单操作类来实现BaseDal和订单接口。这样工作量就少了很多,而且也没有了重复性的代码。还真是非常简单呢。
刘朋:如果新增的实体类特别多的话,意味着我要写大量的接口和操作类,虽然从代码上来说,没有了重复代码,但是我自己做的工作其实也算重复劳动吧。有没有其他方法使我不用做这些重复劳动,或者少重复?
MOL:除了说你们懒,我还能说其他的吗?不过,还真有一种方法,让你们不用去做这样重复的劳动,这就是传说中的T4模板。
观察每个业务接口类的实现,其实它们都是继承了IBaseDal这个父接口,唯一不同的就是指定返回类型的时候不一样。比如“用户接口”的代码是这样的:
public interface IT_CustomerInfo_TBDAL :IDALBase<T_CustomerInfo_TB>
而“订单接口”的代码是这样的:
public interface IT_Order_TBDAL : IDALBase<T_Order_TB>
也就是说,只需要把黑体部分换一下,其他的代码都不需要动。那么,我们就可以考虑做这样一个模板:
public interface I 坑DAL : IDALBase<坑>
然后用对应的实体名称把“坑”填好,就形成了一个实体接口。
我们用T4模板来实现这个功能。
T4模板中的T4是文本模板转换工具(Text Template Transformation Toolkit)的缩写,这4个单词的首字母都是T,所以简写为T4。T4模板的语法和C#语法还是挺像的,属于那种“一看就懂”的技术。关于T4模板的语法不是我们要说的重点,所以在这里不再赘述,大家可以自行去查询,学习一下。这里我们只讲解如何使用T4模板来减少重复劳动,还是以实体接口来举例。
上面已经讲了如何新增一个实体接口,并且完成了一个叫IT_CustomerInfo_TBDAL的实体接口,那么这个接口就可以作为原型来创建模板。
在接口层Elands.JinCard.EF.IDAL中添加一个“运行时文本模板”IDAL.tt,如图3-27所示。
图3-27 添加文本模板
并在这个文件中添加下面的代码:
01 CodeGenerationTools code = new CodeGenerationTools(this); 02 MetadataLoader loader = new MetadataLoader(this); 03 CodeRegion region = new CodeRegion(this, 1); 04 MetadataTools ef = new MetadataTools(this); 05 string inputFile = @"..\\Elands.JinCard.EF.Model\\EFModel.edmx"; 06 EdmItemCollection ItemCollection = loader.CreateEdmItemCollection (inputFile); 07 string namespaceName = code.VsNamespaceSuggestion(); 08 EntityFrameworkTemplateFileManager fileManager = EntityFramework TemplateFileManager.Create(this); 09 #> 10 using Elands.JinCard.EF.Model; 11 using System; 12 namespace Elands.JinCard.EF.IDAL{ 13 <# 14 // Emit Entity Types 15 foreach (EntityType entity in ItemCollection.GetItems<EntityType>(). OrderBy(e => e.Name)) 16 { 17 #> 18 public interface I<#=entity.Name#>DAL : IBaseDal<<#=entity.Name#>> 19 { 20 } 21 <#}#> 22 }
简单说一下:
PS:如果你的项目名称和示例中写得不一样,那么需要把上面提到的这3项换成自己项目中的特定名称。
这个IDAL.tt完成以后,不要进行其他操作,直接保存。然后再来看解决方案管理器,发现IDAL.tt已经变成了一个父节点,如图3-28所示。
图3-28 保存模板后的解
通过图3-28可以很清楚地看到,T4模板已经帮我们生成了与实体相关的所有接口。随便打开一个接口,如IT_Order_TBDAL,其编码如图3-29所示。
图3-29 模板生成的接口
正如前面我们所期望的一样,一个实体业务接口(IT_Order_TBDAL)只需要实现父接口(IBaseDal)即可。
同样的,其他层(Layer)中,只要涉及实体相关的类或接口,都可以通过T4模板来完成,完成的步骤是这样的:
(1)先找一个实体来完成具体的实现。
(2)以这个具体的实现来挖坑。
(3)构造T4模板并保存。
然后,就没有然后了。
刘朋:通过T4模板,我们的懒惰又上升到了一个新的境界,想想就觉得有点小激动呢。
众:嘁~
MOL:强调一下,这里举例是用T4模板来构建类和接口,其实T4模板的本质是用来生成文本文件的,类和接口是.cs后缀的文件,同样的,还可以生成其他类型的文本文件,比如后面我们会使用T4模板来生成XML文件。
冲冲:那什么时候可以用T4模板呢?
MOL:使用T4模板需要具备这样一些条件:①需要做大量重复的工作;②这些重复的工作是有规律可循的;③构建T4模板的时间远比重复劳动的时间少得多。只要具备这些条件,就可以考虑使用T4模板。
MOL在本示例中描述了如何使用T4模板来生成业务接口,下面就请大家使用T4模板来完成DAL层、IBLL层、BLL层。
没多久,鹏辉就一脸苦瓜相地说:一样的代码,为啥我的模板生成不了预期的.cs文件?
MOL打开他的模板文件看了一下也觉得非常神奇,再仔细观察生成的文件,发现文件中存在大量的路径,而且路径还是中文的,问题马上浮出水面。
MOL:号外,号外。注意一下,使用T4模板的时候,大家一定要把项目放在英文路径下面,如果项目所在的路径包含中文,那么T4模板是无法解析的。
在MOL的英明指挥下,经过大家不懈的努力,IDAL、DAL、IBLL和BLL层的结构终于浮出了水面。完成以后的解决方案结构如图3-30所示。
图3-30 完成后的解决方案
通过这个解决方案的结构,我们可以总结一下。对于每一层(Layer),都只是写了一个泛型基类,如IBaseDal,然后用T4模板去遍历实体,生成对应的子类。
也就是说,对于第一层,我们只需要做两件事: 第一件事是写基类,第二件事是写T4模板。
对,就是这两件事,你没有看错。
接下来我们要做的事情就是将已经完成的这个解决方案进行优化,大家先来检查一下哪些代码是可以优化的。
刘朋:前面讲到Spring.NET的时候说,我们可以通过容器来取代传统的new来取得一个实例,但是在模板生成的示例中,大量地存在new一个对象的代码,这些代码是不是可以优化?
MOL:非常正确。
其实new这个东西是比较讨厌的,大多数时候,我们都觉得它是“不可控”的。为什么这样说呢?首先来说一下new操作做了什么动作。
类 classModel=new 类();
这一行代码可能是我们最常见的获取一个实例的方法,程序在执行到这句话的时候,会先在内存中开辟一块空间用来存放实例对象,然后再调用“类”的构造函数对classModel进行初始化。
从表面上看好像没什么不妥。
请注意,这样描述的前提是“在同一个线程内”。如果是多个线程的话会出现什么情况呢?
假设有两个人,分别叫张铁蛋和王二妮,张铁蛋从北京出差到天津,王二妮从某个村里到天津面试。晚上的时候,他们分别要住店(注意:“分别”两个字是重点,大家千万不要想歪了)。不巧的是,天津诺大一个城区,当时只剩下一个空房间。更不巧的是,张铁蛋和王二妮同时到了这家旅店,服务员A和服务员B分别接待张铁蛋和王二妮。
张铁蛋对服务员A说:我需要一间空房。
服务员A说:我们正好还有一间空房。这是房间钥匙。
同一时间,在另一个服务台,王二妮对服务员B说:我需要一间空房。
服务员B说:我们正好还有一间空房。这是房间钥匙。
于是,张铁蛋和王二妮对于空房的使用权上,就有了冲突。最后到底是王二妮住进了房间,还是张铁蛋住进了房间,或是其他的情况,我们暂且不用考虑那么多。
其实在这个示例中,张铁蛋和王二妮就是两个线程。他们去旅店休息开房间,就是去new一个对象实例,而房间就是内存空间。最后房间里是王二妮还是张铁蛋,其实我们都不得而知。但是张铁蛋的领导(进程A)和王二妮的父母(进程B)是不知道这个情况的。他们会按照既定的程序去寻找自己的下属(或女儿)。那么就会造成这样的情况:张铁蛋的领导进入房间见到了王二妮,也可能王二妮的父母进入房间发现了张铁蛋。这种情况下,必然导致主进程出现异常。
也就是说,进程A在取值的时候,有可能取到的值并不是预期想要的值,即专业人事所谓的“线程非安全”的说法。
为了保证进程取值的时候一定是预期的值,也就是说,张铁蛋的领导进入房间,看到的一定是张铁蛋;王二妮的父母进入房间,看到的一定是王二妮。程序员们通常会使用lock关键字来达到这样的目的。也就是说,张铁蛋进入房间以后,先给房间上个锁,保证别人(王二妮)进不去房间。这样,张铁蛋的领导进入房间以后,见到的一定是张铁蛋。而王二妮进不去房间的消息也会被她的父母所知悉,这样就不会造成冲突。
这样会使得王二妮只能睡大街,或者是在房间门外等着,直到房间里的人走掉。这样做的话,很明显,效率是比较低下的。我们更希望在住店之前,领导或父母就已经把房间开好了,到旅店后直接进房间就可以了。
其实由领导或父母去开房间的这个思路,就是在前面讲过的Spring.NET的思想。Spring.NET通过“容器”来管理对象。这里的容器,就是领导或父母。
回到项目中,我们来改造一下BLL层里实例化实体的代码。
现有的代码如下:
public partial class T_Activity_TBService:BaseService<T_Activity_TB>, IBLL. IT_Activity_TBService { public override void SetCurrentDAL() { CurrentDal =new BaseDal<T_Activity_TB>(); } }
获取对应的实体时,使用了new的方式。我们希望对于每一个进程来说,当每次使用CurrentDal的时候,获取到的实体一定是相同的。用new来实例化对象,显然达不到我们的要求,所以改造之。
我们的思路是,构建这样一个容器,这个容器里就已经把所有实体对应的Dal实例化(开好房间)。因为这个容器是操作数据库的,所以我们给这个容器命名为DbSession。这个DbSession的定义是这样的:
public partial class DbSession :IDbSession { //用户实体数据库操作类 private IT_CustomerInfo_TBDAL _T_CustomerInfo_TBDAL; public IT_CustomerInfo_TBDAL T_CustomerInfo_TBDAL { get { if (_T_CustomerInfo_TBDAL == null) { _T_CustomerInfo_TBDAL =new T_CustomerInfo_TBDAL(); } return _T_CustomerInfo_TBDAL; } } //实体的数据库操作类 …… }
这个所谓的DbSession里定义的其实就是一大堆数据库表对应的实体操作类。在这个类里,进行了对实体操作类(房间)的实例化。
同样的,为了减少强依赖,还需要定义一个IDbSession接口,这个接口里定义了有哪些实体操作类。IDbSession的定义是这样的:
public partial interface IDbSession { //用户实体操作类 IT_CustomerInfo_TBDAL T_CustomerInfo_TBDAL { get; } //实体操作类 …… }
DbSession定义好以后,我们就只剩一个问题需要解决了。这个问题就是保证DbSession在每个进程内都是唯一的,并且是仅有的一个。
这个问题确实很头疼。通常情况下想到最直接的办法就是用单例模式(Singleton Pattern)来实现。转念一想,如果真用单例实现了,其实并没有解决问题。因为这样产生的实体操作对象,一定是整个应用程序共用的对象。那么用什么办法来实现呢?
幸运的是,微软已经给我们提供了一个类CallContext,这个类隐藏在System.Runtime.Remoting.Messaging这个几乎没人用的namespace下面。微软在MSDN上对它的描述是这样的:
“提供与执行代码路径一起传送的属性集。无法继承此类。”
基本上每个人看到这样的描述,都会抓狂。再来看看它的备注:
“CallContext是类似于方法调用的线程本地存储区的专用集合对象,并提供对每个逻辑执行线程都唯一的数据槽。数据槽不在其他逻辑线程上的调用上下文之间共享。当CallContext沿执行代码路径往返传播并且由该路径中的各个对象检查时,可将对象添加到其中。”
翻译成大家能听懂的话就是说,CallContext这个类是非常的牛啊,它可以创建一个线程内唯一的容器。大家可以把任何需要的对象放到这个容器内,CallContext可以保证你在线程内获取到的对象一定是唯一的。
好,有了CallContext这个容器,那我们就可以把DbSession放到这个容器中。代码如下:
public class DbSessionFactory { //获取DbSession的方法 public static IDAL.IDbSession GetCurrentDbSession() { //从容器中获取IDbSession IDAL.IDbSession dbSession = (IDbSession) CallContext.GetData ("DbSession"); //如果容器中没有IDbSession对象 if (dbSession == null) { //创建一个IDbSession对象 dbSession =new DbSession(); //把新创建的对象放到容器中 CallContext.SetData("DbSession",dbSession); } //将IDbSession对象返回 return dbSession; } }
这样就完成了让DbSession对象“线程内唯一”的任务。
可以看到,我们把获取DbSession对象的方法放在一个叫DbSessionFactory的工厂类里面。没错,这就是传说中的工厂模式。是不是感觉自己变得“高大上”起来了呢?
有了这些思路,我们再来重新构建一下解决方案。把IDbSession放在IDAL层中,再新建一个项目Elands.JinCard.EF.Factory用来存放DbSession和DbSessionFactory。因为IDbSession和DbSession中描述的是与数据库表对应实体有关的业务类,所以也可以使用T4模板来减少重复劳动。
构建完的解决方案如图3-31所示。
图3-31 加入了线程唯一
接下来就可以修改BLL层中关于获取DAL对象的代码了。还是以现有代码进行分析:
public override void SetCurrentDAL() { CurrentDal =new BaseDal<T_CustomInfo_TB>(); }
我们不希望用new的方式来获取一个CurrentDal对象,那么就需要通过DbSession的方式来获取。首先在BLL层的BaseService中增加关于DbSession的定义。
public IDAL.IDbSession DbSession = DalFactory.DbSessionFactory.GetCurrent DbSession();
在每一个子类中,获取CurrentDal的方法就变成了:
CurrentDal = DbSession.T_CustomerInfo_TBDAL;
对应修改T4模板的代码就比较简单了,这里不再多说。
到这里为止,“干燥”(DRY)就基本上完成了。回顾一下我们的解决方案解决了哪些问题。
解决完这些问题,我们就可以小试牛刀了,比如大家可以在数据库中随意地添加一些数据表,体会一下不用多写SQL语句的好处……
大家听完MOL侃大山以后,纷纷拿起自己的键盘啪啪啪地敲了起来。不一会问题就出现了。
冲冲:我们在写SQL语句的时候,可以很方便地指定查询条件,比如,我要查询张三的用户信息,那么我的SQL语句可以这样写:
Select * from T_CustomerInfo_TB where CustomerName='张三'
但是咱们刚刚讲的内容中,并没有提到关于查询条件怎么实现。
MOL:这个问题非常好。其实数据库的操作里面,最难的也就是查询,因为查询的条件是非常不固定的。那我们应该如何解决“查询”的问题呢?
其实关于查询的操作不仅仅有过滤条件,还有排序条件、查询结果字段、是否分页。接下来就分别来解决这3个问题。
先以查询整个表的所有字段、不分页来说明过滤条件的使用方法。
回想一下,如果我们在数据库中查询表T_CustomerInfo_TB的数据,可以使用SQL语句来“搞定”:
Select * from T_CustomerInfo_TB
对应到解决方案中,我们提供了一个GetListAll()的方法来查询T_CustomerInfo_TB中所有的数据。这个方法的实现如下:
public IList<T> GetListAll() { return context.Set<T>().ToList(); }
如果要在数据库中查找张三的信息,那么可以通过SQL语句“搞定”,SQL语句如下:
Select * from T_CustomerInfo_TB where CustomerName='张三'
无非就是在查询所有结果的SQL语句后面加了一个where条件进行过滤。
那么,大家可以脑洞大开想一下,context.Set<T>()是不是也可以加一个where条件进行过滤呢?来,试着在context.Set<T>()后面打个“.”,看看微软有没有提供类似where的方法。
仔细一看,还真有个Where()方法,如图3-32所示。
图3-32 Where()方法
根据提示,Where()方法有4个重载,选一个最简单的重载来看,这个重载需要一个类型为Expression<Func<T,bool>>的参数。大多数人看到这个参数的时候,脸上会出现一个大写的懵。千万不要紧张,将这个参数类型分开来看,第一部分Expression<?>表示当前参数是一个表达式。Fun<T,bool>是描述了这个关于“源”的表达式,必须是一个bool值。这里的“源”,其实就是context.Set<T>。那么,这个表达式到底长啥样呢?说白了,其实就是Lambda表达式。例如,要查询张三的信息,即context.Set<T>().Where(t=>t.CustomerName=="张三")。那么,一个关于条件查询的函数,我们暂且定义它为GetList,这个函数就可以如下实现:
public IList<T> GetList (Expression<Fun<T,bool>> whereLambda) { return context.Set<T>().where(whereLambda).ToList(); }
Lambda表达式不是我们的重点,而且它本身也不太难,所以这里不做过多的讲解。
通过一个Lambda表达式,就完成了对复杂查询的抽象。
有时我们不会查询一个表中的所有字段,比如我需要知道张三的姓名和手机,可以通过下面的SQL语句来搞定。
Select Customername,Phone from T_CustomerInfo_TB where CustomerName='张三'
也就是说,我们需要查询得到具体的字段,那么如何把这些具体的字段抽象出来呢?要知道,查询的字段结果可是非常灵活的,有可能今天需要知道姓名和手机,明天就需要知道性别和地址。
还是刚才的思路,我们试着找一下context.Set<T>()是否提供了查询结果的方法。
微软想得还是很周到的,它已经给我们提供了一个叫Select()的方法,如图3-33所示。
图3-33 Select()方法
Select()方法需要的参数是一个叫Expression<Fun<T,TResult>>类型的参数。同样的,大家只需要把这个参数理解成是一个Lambda表达式就可以了。那么,查询所有用户的“公司名称”和“地址”的代码就是:
context.Set<T_CustomInfo_TB>().Select(t => new {t.CompanyName, t.MyAddress }).ToList();
如果要查询张三的公司名称和地址,那么代码就是:
context.Set<T_CustomInfo_TB>().Where(t=>t.CustomerName=="张三").Select(t=> new { t.CompanyName, t.MyAddress }).ToList();
因为查询得到的结果千奇百怪,所以我们也不从业务层面去抽象这些查询结果了,直接用一个dynamic类型来表示。dynamic表示一个“动态”的类型,这正好与我们的设计不谋而合。查询指定字段(属性)的方法如下:
public IList<dynamic> GetList(Expression<Fun<T,TResult>>selectLambda,Expression<Fun<T,bool>> whereLambda) { return context.Set<T>().where(whereLambda).Select(selectLambda).ToList(); }
同样的思路,如果需要排序,那么就加上排序条件的Lambda,关于排序的代码是这样的:
Public IList<T> GetList(Expression<Fun<T,Tkey>> orderLambda) { //升序 return context.Set<T>().OrderBy(orderLambda); //降序 return context.Set<T>().OrderByDescending(orderLambda); }
分页功能也是项目中必不可少的一部分。如果用SQL来实现分页,以第3页的10行数据来举例,SQL语句是这样的:
SELECT * FROM T_CustomerInfo_TB ORDER BY CustomerName OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
这个OFFSET语法是SQL Server 2012里的新特性。如果用SQL Server 2005或SQL Server2008,那么可以使用Row_Number()函数。如果是SQL Server 2000,呵呵,那你只能通过临时表或视图来解决了。这里以SQL Server 2012的版本来说明。
上面的SQL语句表示,先跳过20行,再取接下来的10行数据,这就是第3页的10行数据。
在EF里也是这个思路,只不过语法稍微有些不一样。EF里面的代码是这样的:
context.Set<T>().Skip((pageIndex - 1) * pageSize).Take(pageSize)
同样的,上面这行代码描述的也是跳过前面几页的数据,再获取接下来的几行数据。
pageIndex表示页码,pageSize表示获取数据的条数。
到这里为止,关于过滤、排序、查询动态结果、分页的功能就都讲完了。接下来我们要把这些功能都整合一下,变成几个比较“牛”的查询函数。这里之所以是几个而不是一个,是因为:
在这里我们整合这样一个函数,查询一个表实体、有过滤条件、带排序和分页功能。其他的几个函数请大家自行整合。整合后的函数如下:
public IQueryable<T> GetListByPage<TKey>(Expression<Func<T, T>> selectLambda, Expression<Func<T, bool>> whereLambda, Expression<Func<T, TKey>> orderLambda, int pageSize, int pageIndex, out int total, bool isAsc) { total = context.Set<T>().Where(whereLambda).Count(); var result = context.Set<T>().Where(whereLambda); if (isAsc) { result = result.OrderBy(orderLambda); } else { result = result.OrderByDescending(orderLambda); } return result.Skip((pageIndex - 1) * pageSize).Take(pageSize).Select (selectLambda).AsQueryable<T>(); }
到这里为止,查询功能就完成了。查询功能是到目前为止我们项目中最麻烦的一个内容。
EF提供了3种方式来进行数据的持久化,分别是DataBase-First(数据库先行)、Model-First(模型先行)、Code-First(代码先行)。
最常用的是数据库先行和模型先行两种方式。这3种方式的优缺点如下: