前言:本篇主要讲述数据访问层的开发,而且为了大家交流,已经创建企业项目开发团队,希望大家也以后会把有关企业开发的文章放入团队中,希望大家积极参加这个团队。而且我以后也会发表更多的项目示例,大家一起学习进步!
本篇的话题主要如下:
问题提出
设计方案
问题提出
数据访问层(DAL)的目标创建一些以便业务层来调用的类和方法。我们之前总是用GridView来绑定DataSet和DataReader,但是在稍微大点的项目开发中DAL不能直接和用户
界面打交道。
一般来说,DAL是用来和数据库和BLL打交道的,也就是处理BLL和数据库的中间。数据以什么形式在DAL和BLL之前传递有很多的争论。不同的人有不同的意见,数据传递的形式有:DataSet,强类型的DataSet,DataReader,自定义实体。在介绍Ling to Sql之后,大家心里会有清晰的答案。在以前的开发中,我们一般是采用ADO.NET来和数据库打交道,那么就需要我们的开发人员对ADO.NET有一定的比较深入的了解,但是当我们用Linq to Sql之后,我们可以很方便的使用DataContext来与数据库拉打交道,而不需要我们懂得很多的ADO.NET的知识,但是在Linq to Sql的背后还是在采用ADO.NET来和数据库交互的。
还有就是事务处理的问题。关于事务的概念,相信大家都清楚,我也不赘述了。事务处理在什么地方实现有如下意见:在存储过程中直接用SQL语句来写;在DAL层处理,
在BLL层处理。当然,每一种的选择都有各自的理由和利弊。还有一点要注意的是:不要把事务处理的代码到处写,如在DAL层中写一点,在BLL中写一点。
设计方案
在设计方案中实际上就是提供几个选择来解决之前我们提出的问题。以下就是两个选择:
1.DAL只要是执行CRUD操作,CRUD是就是:Create,Read,Update,Delete.在.NET Framwework中提供了很多和数据库打交道的ADO.NET类和方法,如
SqlConnection,SqlCommand,SqlCommand.ExecuteNonQuery()等,用过ADO.NET的朋友应该清楚这些常用的类,我这里也不罗嗦。
2.SqlHelper
用过ADO.NET的朋友应该知道,在我们开发过程中,很多时候写ADO.NET代码的时候,代码结构和功能都是大同小异的,所以基于此,微软就开发了Microsoft Data Access Application Block,只要我们调用其中的一些方法,传入一些参数就行了,不需要我们再去写那些繁琐的ADO.NET代码,因为这个数据访问块都已经封装好了。其中一个最重要的类就是SqlHelper.这个类是个静态类,提供了很多的方法,如下:
ExecuteNonQuery
ExecuteDataset
ExecuteReader
ExecuteScalar
FillDataset
上面 方法的设计包含了很多OO的思想。我们来看看ExecuteNonQuery方法 ,其他的方法的设计思想和方式一样的:
//连接字符串
public static int ExecuteNonQuery(string connectionString, CommandType commandType,string commandText)
{ … }
public static int ExecuteNonQuery(string connectionString, CommandType commandType,string commandText, params SqlParameter[] commandParameters)
{ … }
public static int ExecuteNonQuery(string connectionString, string spName, params object[] parameterValues)
{ … }
//连接对象
public static int ExecuteNonQuery(SqlConnection connection, CommandType commandType, string commandText)
{ … }
public static int ExecuteNonQuery(SqlConnection connection, CommandType commandType, string commandText, params SqlParameter[] commandParameters)
{ … }
public static int ExecuteNonQuery(SqlConnection connection, string spName, params object[] parameterValues)
{ … }
//事务对象
public static int ExecuteNonQuery(SqlTransaction transaction, CommandType commandType, string commandText)
{ … }
public static int ExecuteNonQuery(SqlTransaction transaction, CommandType commandType, string commandText, params SqlParameter[] commandParameters)
{ … }
public static int ExecuteNonQuery(SqlTransaction transaction, string spName, params object[] parameterValues)
{ … }
上面前3个方法都是采用了一个连接字符串的参数,而接下来的3个方法是采用了一个连接对象为参数,最后的3个方法采用了一个事务对象为参数。这些方法在调用存储过程时提供了很大的灵活性。 例如:
CREATE PROCEDURE UserAccountInsert(
@Name varchar(50),
@UserAccountId int OUTPUT
)
AS
SET NOCOUNT ON
INSERT INTO UserAccount (Name) VALUES (@Name)
SET @UserAccountId = Scope_Identity()
调用代码如下:
public int InsertUserAccount(string connectionString, string name)
{
SqlParameter[] parameters =
{
new SqlParameter( "@Name", SqlDbType.VarChar, 50),
new SqlParameter( "@UserAccountId", SqlDbType.Int)
};
parameters[0].Value = name;
parameters[1].Direction = ParameterDirection.Output;
SqlHelper.ExecuteNonQuery(connectionString,CommandType.StoredProcedure, "UserAccountInsert", parameters);
return Convert.ToInt32(parameters[1].Value);
}
还有一个问题要注意就是更新时的同步问题。例如,用户从数据库中获取一条数据,然后更改了一些内容,之后就保存记录到数据库中,那么系统就应该只保存用户之前取出的那条数据。为了达到这个效果,我们可以在每次数据更新的时候添加一个datetime类型或者int类型的字段来标记。当我们从数据库中返回一条记录时,我们在UI显示层显示出来,然后修改数据,再保存到数据库中,那么我们在数据库中的存储过程的SQL语句就只是更新之前我们取出的那条数据。在Sql Server 2005中,我们可以用timespan类型的字段来跟踪和标识每条数据的版本,所以sql语句如下:
CREATE PROCEDURE UserAccountUpdate(
@Name varchar(50),
@UserAccountId int,
@LastUpdateDate datetime
)
AS
UPDATE UserAccount
SET Name = @Name,
LastUpdateDate = GetDate()
WHERE UserAccountId = @UserAccountId
AND LastUpdateDate = @LastUpdateDate
请大家注意LastUpdateDate 字段其实就是一个标识每条数据版本的辅助字段。
调用的C#代码如下:
public bool UpdateUserAccount(string connectionString, string name, int userAccountId, DateTime lastUpdateDate)
{
SqlParameter[] parameters =
{
new SqlParameter( "@Name", SqlDbType.VarChar, 50),
new SqlParameter( "@UserAccountId", SqlDbType.Int),
new SqlParameter( "@LastUpdateDate", SqlDbType.DateTime)
};
parameters[0].Value = name;
parameters[1].Value = userAccountId;
parameters[2].Value = lastUpdateDate;
int rowsAffected =
Convert.ToInt32(SqlHelper.ExecuteNonQuery(connectionString,
CommandType.StoredProcedure, "UserAccountUpdate", parameters));
return rowsAffected == 1;
}
下面我们就来看看数据在DAL和BLL之间是以什么形式来交换的,一般有以下选择:
DataSet
类型化的DataSet
自定义实体
当BLL类调用从DAL中的一些方法拉获取数据时,它们将怎样接受这些数据?
是用DataSet/DataTable,还是自定义实体?
下面我们就看看给自的优缺点:
如果选择使用DataSet/DataTable在DAL和BLL层之前传递数据,就需要使用ADO.NET中的方法来访问BLL中的数据。如果选择自定义实体,那么所有的数据将被封装到自定义实体类和类的集合中,这样就可以根据具体情况来访问BLL,这中方式更加的自然。
很多人认为DataSet/DataTable对于基于桌面的只能客户端程序来说是最好的选择,但是对于可扩展的高性能Web网站来说,则不够强大。这里所说的DataSet是指类型化的DataSet,因为非类型化的DataSet有很多的确定:进行配置和编码时,很容易把表名,字段名,关系名,或者字段的类型搞错,而且在调试的时候花费很多的时间。而类型化的DataSet很容易使用,因为它可以使用智能感应来获得字段名,而且还内置了排序和过滤的功能,执行数据绑定而且DataSet/DataTable还是可以序列化的,这样就可以更加容易的传输它们。
DataSet/DataTable的缺点:性能和扩展性的局限性,数据的表示形式和业务规则验证。如果只需要传递一条数据,那么我们仍然需要创建一个完整的DataSet/DataTable,这就需要系统开销。而且DataSet/DataTable和数据库的关于很密切,可以说是内存中的数据库,所以DataSet/DataTable没有一个清晰的,面向对象的数据表示方式。尽管DataSet/DataTable同IDE集成的很好,但是每次数据库结构发生变化,如添加字段,重命名等,那么我们就得重新创建类型化的DataSet.最大的问题就是:在DataSet中添加自定义的业务和验证逻辑困难。在将它们保存到数据库之前,或者在运行其他语句之前要对那些新记录强制之心业务规则,需要写很多的代码,而且需要深入的了解ADO.NET的知识。大家要明白 如果使用的是自定义的实体,那么我们就需要手动的把数据库中的表映射为自定义的业务类,而且这将是一项麻烦的事情,而且还有一点很之前的DataSet/DataTable一样:如果数据库中的表结构变化,那么我们又得重新修改我们的自定义的业务类,
我们真的就没有办法了吗?
有,绝对有方法可以解决上面的问题,那么就Linq to Sql 和它的ORM设计器。
下面我们就看看Linq to Sql
注:在本章中,不需要大家对Linq很熟悉,只要了解就行了,因为对与用到的知识,会详细的讲述的。而且大家一定要一步步的跟着做,切记!
在VS2008中,一个改进的功能就是Linq还有就是对象关系模型(ORM)设计器,ORM设计器可以自动的生成类,并且自动进行ADO.NET的操作。其中一个最中要的类就是DataContext,大家可以把这个类和我们之前谈的SqlHelper类来类比,它们都是封装了数据操作的细节。
下面请大家跟着我一起来做:
1.创建一个新的Sql Server 2005 数据库,或者直接用VS2008自带的SQL Express创建也行,如下:
2.数据库名称为HRPaidTimeOff
3.创建ENTUserAccount表,如下:
我们在这个项目中的所有表都以ENT为前缀,表明这个表是可重用企业级框架的一部分。还有ENTUserAccountId是identity的。
来看看表的定义和字段的意义:这个表其实就是一个用户账户表,其中ENTUserAccountId就是用户账户的ID,也是主键,WindowsAccountName就是用户的计算机名称,FirstName,LastName就是用户名字,Email用户邮箱,IsActive表明这个用户是否是激活状态,后面的IsertDate,InsertENTUserAccountId,UpdateDate,UpdateENTUserAccountId,
这几个字段在我们项目的所有的表中都有的,因为我们之后要添加审计和跟踪功能要用到这些字段,它们的意义分别是:用户添加的时间,是哪个用户添加了当前的这个用户,用户更新时间,是哪个用户更新的当前用户。举个例子就是:加入Mary是管理员,她添加了一个Bob用户,那么ENTUserAccountId就是Bob用户记录的主键,FirstName就是Bob,InsertENTUserAccountId就是Mary的ID。
下面我们就添加一条记录:
WindowsAccountName = Lufy
FirstName = Yang
LastName = Wang
Email = yangyang4502@yahoo.com.cn
IsActive = True
InsertDate=getdate()
InsertENTUserAccountId = 1
UpdateDate = getdate()
UpdateENTUserAccountId = 1
现在数据表就创建好了,打开VS2008创建连接(如果我们之前是直接用VS2008的服务器资源管理创建的,那么下面的步骤就不用作了,如果使用的 sql 2005,下面的步骤就要做
1.在VS2008中选择"视图"-"服务器资源管理"
2.点击"数据库连接",右击,"添加连接",数据库选择"Microsoft SQL Server (SqlClient)",服务器为".\sqlexpress".
3.选择身份验证方式为"Windows验证",选择数据库为"HRPaidTimeOff"
4.测试连接,然后OK
下面我们就来创建DataContext
我们之前说过:DataContext和我们之前谈的SqlHelper类来类比,它们都是封装了数据操作的细节。我们在V2.PaidTimeOffDAL项目上右击,选择"添加新项",如下:
VS2008自动的添加System.Data.Linq的引用,同时也添加了三个文件:HRPaidTimeOff.dbml, RPaidTimeOff.dbml.layout和HRPaidTimeOff.designer.cs.其中HRPaidTimeOff.dbml, RPaidTimeOff.dbml.layout将会被ORM图形设计器所使用,.cs文件包含了所有自动创建的类。
大家可以双击"HRPaidTimeOff.designer.cs"文件,我们就会看见VS2008创建的分部类HRPaidTimeOffDataContext,这个类继承自System.Data.Linq.DataContext。我们可以用这个类来和数据打交道,就想我们之前使用SqlHelper类一样。这个类有一个名为MappingSource的变量,和一些构造函数。其中构造函数有的采用一个连接字符串为参数,有的用一个实现了IDBConnection接口的类为参数,然后所有的构造函数都调用了OnCreated方法。
打开折叠区域--"Extensibility Method Definitions",就可以看到下面的方法:
这是VS2008的一个新的语法:分部方法,大家可以和之前的partial class类比。这个方法允许你在类中先定义方法,然后在该类的其他partial类中实现方法体。如果我们的partial 方法没有实现,那么及时你调用了这个方法,也不会出错,因为编译器会忽略这个方法的调用。
下面,我们双击"HRPaidTimeOff.dbml",就可以看到图形化的ORM设计器。然后,我们展开"服务器资源管理器"窗口,然后把PaidTimeOff数据库总的ENTUserAccount拖到设计器的左边。如下:
此时,VS2008就直接创建一个实体类来映射ENTUserAccount表,再次打开"HRPaidTimeOff.designer.cs"文件,我们可以看到这个文件和我们之前看到的就不同了,我们首先可以看到下面的Attribute被添加了:
而且在"Extensibility Method Definitions:"很多新的方法也添加了:
partial void UpdateENTUserAccount(ENTUserAccount instance);
partial void DeleteENTUserAccount(ENTUserAccount instance);
而且,一个新的构造函数也添加了,这个函数采用一个连接字符串为参数和一个映射源为参数:
{
OnCreated();
}
除此之外,还有一些类添加了,在解决方案窗口,我们可以看到Settings.settings文件和Settings.Designer.cs文件,打开Settings.Designer.cs文件,我们可以看到这个类是继承自global::System.Configuration.ApplicationSettingsBase,有一个属性很重要,就是HRPaidTimeOffConnectionString,这属性返回数据库连接字符串 ,所以在默认情况下,我们之前建立的HRPaidTimeOffDataContext的数据库连接字符串在Settings文件中指定。
我们再回到HRPaidTimeOffDataContext类,我们可以找到一个ENTUserAccount的类,它就是数据库表的一个映射类。
我们只要了解这么多就行了,下面就通过例子的使用来了解其他的知识。
添加数据记录
编译V2.PaidTimeOffDAL,我们在我们解决方案的那个网站项目这引用V2.PaidTimeOffDAL.dll,我们这里只是临时的使用以下V2.PaidTimeOffDAL.dll而以,大家知道UI层不能直接和DAL打交道的,我们这里暂时的使用,使得大家对Linq更加的熟悉一点。
大家跟着做:
1.在网站项目中添加System.Data.Linq引用。
2.在Default.aspx页面添加一个按钮,ID为btnInsert,Text为Insert
3.在Default.aspx.cs添加V2.PaidTimeOffDAL引用。
4.在Insert按钮点击事件下,添加下面代码:
protected void btnInsert_Click(object sender, EventArgs e)
{
//创建DataContext实例
HRPaidTimeOffDataContext db = new HRPaidTimeOffDataContext();
//创建新的ENTUserAccount 对象,并且设计属性
ENTUserAccount userAccount = new ENTUserAccount
{
WindowsAccountName = @"VARALLO1\VaralloMadison",
FirstName = "Madison",
LastName = "Varallo",
Email = "madison.varallo@v2.com",
IsActive = true,
InsertDate = DateTime.Now,
InsertENTUserAccountId = 1,
UpdateDate = DateTime.Now,
UpdateENTUserAccountId = 1
};
//传入数据
db.ENTUserAccounts.InsertOnSubmit(userAccount);
//保存到数据库
db.SubmitChanges();
}
上面的代码其实和我们之前使用ADO.NET的形式很接近。首先我们创建一个连接实例HRPaidTimeOffDataContext和数据库连接,然后就是创建一条数据,然后调用InsertOnSubmit方法插入数据,最后就保存数据,db.SubmitChanges()把数据保存到数据库中。
更新数据
在Default.aspx页面中再添加一个按钮,ID为btnUpdate,Text为Update,按钮事件代码如下:
protected void btnUpdate_Click(object sender, EventArgs e)
{
HRPaidTimeOffDataContext db = new HRPaidTimeOffDataContext();
ENTUserAccount userAccount = db.ENTUserAccounts.Single(u =>
u.WindowsAccountName == @"VARALLO1\VaralloMadison");
userAccount.IsActive = false;
db.SubmitChanges();
}
这个段代码和之前的差不多,首先还是实例化一个数据库连接的对象HRPaidTimeOffDataContext ,然后用Lamdal表达式选出一条数据,然后更改IsActive =false;最后保存回数据库中。
大家可以看到,使用Linq以后,我们再也没有用ADO.NET的语句了,而且我们写代码的方式也完全改变。
我们从更新数据的例子中看到,我们首先从数据库中获取一条数据,然后改变数据,最后再次保存回数据库。对于Web程序来说用,我们通常从数据库中获取一条数据,显示在界面上,然后用户更改数据,然后点击按钮回传到服务器,那么数据就传到了我们的DAL,DAL取执行更新操作。例如,我们把一个用户的信息全部取出了,包含FirstName,LastName,Email,IsActive等等,那么我们的界面上面就显示出来这些数据,我们假设是用TextBox来显示的,那么当我们更改了其中一个数据,如IsActive,那么我们就点击按钮,我们此时其实就要把界面上面的所有信息,包括FirstName,LastName,WindowAccountName,Email等全部返回到服务器(大家想想我们的一般更新的存储过程是怎样写的,就明白了),因为如果我们只是返回一个IsActive,服务器端根本不知道更新那条数据,此时我们在服务器端就应该写下面的代码:
protected void btnUpdate_Click(object sender, EventArgs e)
{
//Create an ENTUserAccount object and set the properties
ENTUserAccount userAccount = new ENTUserAccount
{
ENTUserAccountId = 2,
WindowsAccountName = @"VARALLO1\VaralloMadison",
FirstName = "Madison",
LastName = "Varallo",
Email = "madison.varallo@v2.com",
IsActive = false,
UpdateDate = DateTime.Now,
UpdateENTUserAccountId = 1
};
HRPaidTimeOffDataContext db = new HRPaidTimeOffDataContext();
db.ENTUserAccounts.Attach(userAccount, true);
db.SubmitChanges();
}
这段代码和之前一样,也是把IsActive设置为false,但是我们调用的是Attach方法,而且把第二个参数设置为true,说明我们要采用更新操作。
如果运行代码,会报错的:
An entity can only be attached as modified without original state if it declares a version member or does not have an update check policy.
意思是说,我们回传的那条记录在数据库中不存在。
这里还有一个问题要说明:我们确实是从数据库中获取一条数据,然后显示在界面上,我们更新数据后,数据要取数据库中更新,那么数据库怎么知道现在我们传来的数据就是之前从数据库中取出的那条呢?
所以我们就需要一个标识或者说是时间戳,来表明我们现在回传的数据确实属于数据库,那么我们就修改我们的数据表ENTUserAccount,添加一个新的字段Version,类型为timespan。所以当我们取数据的时候,我们就把数据的版本Version字段保存在ViewState中,然后更新数据之后,我们把这个字段一起返回,数据库就检查我们传回的Version字段是否和我们要更新的数据记录的Version是否相同,如果相同就更新数据,如下代码:
Page_Load事件中把Version保存:
protected void Page_Load(object sender, EventArgs e)
{
HRPaidTimeOffDataContext db = new HRPaidTimeOffDataContext();
ENTUserAccount userAccount = db.ENTUserAccounts.Single(ua =>
ua.WindowsAccountName == @"VARALLO1\VaralloMadison");
ViewState["ENTUserAccountId"] = userAccount.ENTUserAccountId;
ViewState["InsertENTUserAccountId"] =userAccount.InsertENTUserAccountId;
ViewState["InsertDate"] = userAccount.InsertDate;
ViewState["Version"] = userAccount.Version;
}
在btnUpdate按钮事件中,如下:
protected void btnUpdate_Click(object sender, EventArgs e)
{
//Create an ENTUserAccount object and set the properties
ENTUserAccount userAccount = new ENTUserAccount
{
WindowsAccountName = @"VARALLO1\VaralloMadison",
FirstName = "Madison",
LastName = "Varallo",
Email = "madison.varallo@v2.com",
IsActive = false,
UpdateDate = DateTime.Now,
UpdateENTUserAccountId = 1
};
userAccount.ENTUserAccountId =Convert.ToInt32(ViewState["ENTUserAccountId"]);
userAccount.InsertENTUserAccountId =
Convert.ToInt32(ViewState["InsertENTUserAccountId"]);
userAccount.InsertDate =Convert.ToDateTime(ViewState["InsertDate"]);
userAccount.Version = (Binary)ViewState["Version"];
HRPaidTimeOffDataContext db = new HRPaidTimeOffDataContext();
db.ENTUserAccounts.Attach(userAccount, true);
db.SubmitChanges();
}
这样就OK了。如果大家有什么问题,就留言!
删除记录:
protected void btnDelete_Click(object sender, EventArgs e)
{
HRPaidTimeOffDataContext db = new HRPaidTimeOffDataContext();
//Create an ENTUserAccount object
ENTUserAccount userAccount = new ENTUserAccount();
userAccount.ENTUserAccountId =
Convert.ToInt32(ViewState["ENTUserAccountId"]);
userAccount.Version = (Binary)ViewState["Version"];
db.ENTUserAccounts.Attach(userAccount);
db.ENTUserAccounts.DeleteOnSubmit(userAccount);
db.SubmitChanges();
}
方法DeleteOnSubmit是自动生成的。我们也可以定制。我们后面谈。
查询数据:
我们可以根据条件查询,然后绑定:
protected void btnSelect_Click(object sender, EventArgs e)
{
HRPaidTimeOffDataContext db = new HRPaidTimeOffDataContext();
var userAccounts =
from u in db.ENTUserAccounts
select u;
GridView1.DataSource = userAccounts;
GridView1.DataBind();
}
今天我们就说到这里,谈了一些方法和预备的方案,我们下篇就说说在我们这个项目DAL到底如何设计。