购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.5 我不想写SQL语句

刘朋:通过前面的讲解,我知道了,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,实体关系映射)。

3.5.1 什么是ORM

从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)。

3.5.2 ORM之iBATIS.NET

从严格意义上来说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更像是一个通过面向过程的方式来实现面向对象的目的。

3.5.3 ORM之NHibernate

接触过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)编写前台页面。

1. 编写实体文件和映射文件

在这个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. 编写DAO(数据库访问对象)

第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步。这一步,我们将做一个前台页面来展示查询得到的数据。

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键运行程序,就可以看到运行结果了。

3.5.4 ORM之EF

刘朋:什么呀,我就想查个数据,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。

1. 创建实体层

创建一个项目,名为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的实体层建立,是不是非常简单?

2. 创建数据操作层

如果说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对象提供了很多方法用来操作数据。常见的方法如下。

只需要这两个方法就足以应付绝大多数的需求了,是不是很简单?

3. 创建业务逻辑层

这一步是最简单的,只需要创建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  } 
4. 创建视图层

视图层主要是用来与客户交互的,在本示例中,我们只简单地把数据库中的数据展示出来。因为新建控制器和视图的过程在前面都已经讲过了,所以此处略去不讲。

创建好的控制器如下:

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=&quot;data source=.;initial catalog= JinCardDB;persist security info=True;user id=sa;password=000;Multiple ActiveResultSets=True;App=EntityFramework&quot;" providerName="System.Data. EntityClient" /> 
  </connectionStrings> 

黑体部分是新加的代码,这段代码千万不要自己写。找到实体层中的app.config配置文件,把app.config中关于数据库连接的描述粘贴到当前web.config中即可。

运行程序,perfect!运行结果如图3-25所示。

图3-25 EF的Demo运行结果

这个运行结果虽然“丑陋”,但它是我们对EF的理解迈出的重要一步,此处应该有掌声。

3.5.5 懒人无敌

冲冲:这样实现起来确实是更面向对象了。

MOL:说重点!

冲冲:使用更方便了。

MOL:说重点!!

冲冲:好吧,更简单了。

MOL:嗯。(心满意足状)

鹏辉:这样确实很简单了。我们只需要操作对象就可以实现数据的增、删、改、查。但是我还有个问题。

MOL:(一脸惊恐状)什么问题?

鹏辉:针对第一个数据库表,我都要去实现对它的操作,但这些操作基本上都是一样的,如果我针对每个表都去写类似的代码,那这样不就违反“干燥”(DRY)原则了吗?

刘朋:哎哟喂,说得还真有那么点道理。

MOL:岂只是有点道理,简直就是真理嘛。接下来我们就来看看如何对一个“湿”的设计进行“干燥”处理。

1. 抽象业务类

正如鹏辉所说,几乎所有的实体类的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 抽象基类后的代码结构

2. 只写一个业务类

鹏辉:如果我要新增订单类的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模板是无法解析的。

3. 去掉讨厌的new

在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语句的好处……

3.5.6 完成查询操作

大家听完MOL侃大山以后,纷纷拿起自己的键盘啪啪啪地敲了起来。不一会问题就出现了。

冲冲:我们在写SQL语句的时候,可以很方便地指定查询条件,比如,我要查询张三的用户信息,那么我的SQL语句可以这样写:

Select * from T_CustomerInfo_TB where CustomerName='张三'

但是咱们刚刚讲的内容中,并没有提到关于查询条件怎么实现。

MOL:这个问题非常好。其实数据库的操作里面,最难的也就是查询,因为查询的条件是非常不固定的。那我们应该如何解决“查询”的问题呢?

其实关于查询的操作不仅仅有过滤条件,还有排序条件、查询结果字段、是否分页。接下来就分别来解决这3个问题。

1. 过滤条件

先以查询整个表的所有字段、不分页来说明过滤条件的使用方法。

回想一下,如果我们在数据库中查询表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表达式,就完成了对复杂查询的抽象。

2. 查询结果及排序

有时我们不会查询一个表中的所有字段,比如我需要知道张三的姓名和手机,可以通过下面的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); 
} 
3. 分页

分页功能也是项目中必不可少的一部分。如果用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表示获取数据的条数。

4. 查询整合

到这里为止,关于过滤、排序、查询动态结果、分页的功能就都讲完了。接下来我们要把这些功能都整合一下,变成几个比较“牛”的查询函数。这里之所以是几个而不是一个,是因为:

在这里我们整合这样一个函数,查询一个表实体、有过滤条件、带排序和分页功能。其他的几个函数请大家自行整合。整合后的函数如下:

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>(); 
} 

到这里为止,查询功能就完成了。查询功能是到目前为止我们项目中最麻烦的一个内容。

3.5.7 数据库先行、模型先行、代码先行

EF提供了3种方式来进行数据的持久化,分别是DataBase-First(数据库先行)、Model-First(模型先行)、Code-First(代码先行)。

最常用的是数据库先行和模型先行两种方式。这3种方式的优缺点如下: TvcUWnuBh2IfoxPM/892gOl75yK5lkUY6wOBbu8jK9X5l0AueJeMM53TZ4V76XeA

点击中间区域
呼出菜单
上一章
目录
下一章
×