通常网站由多个单独的模块组合而成,然而,所有的模块都有以下共同的“设计问题”需要解决:
- 从业务逻辑代码和表示层代码(用户界面)中分离出数据访问代码,增强网站的可维护性和可扩展性。这称为多层设计。
- 使数据访问架构独立,以支持不同的底层数据库---即底层数据库发生改变时不需要对业务对象层进行修改。(每一层的相对独立变动,不影响其他层的改变)。这称为层去耦。
- 设计业务对象架构,以面向对象的方式来暴露从数据访问层取得的数据。这个过程就是将关系型数据映射到oop类上。(ORM[Object Relational Mapper]对象关系映射)
- 支持业务对象缓存,以存储从数据库中取得的数据,从而提高性能。
- 对异常和其他重要的事件(如删除记录)进行处理和记录,方便错误诊断和提供审计追踪。
- 网站和模块的配置信息保存到容易读写的地方。(Web.config)
- 将许多用户界面控件同从业务逻辑层获取的数据进行绑定。
用户界面层主要用于展示数据;业务逻辑层(BLL)主要用于实现业务规则和操作数据;数据层只用于保存数据。
设计分层结构
- 数据存储层:保存数据的地方。(关系型数据库/XML文件/文本/Access等)
- DAL层:对数据存储中的原始数据进行操作的代码。
- BLL层:获取DAL层检索到的数据,并用更抽象、直观的方式显示到客户端,达到隐藏底层的细节(数据库结构),并可以增加逻辑验证以保证输入的安全性和一致性。
- 显示层(用户界面):定义用户在屏幕上可以看到的内容,包括格式化的数据和系统导航菜单。
设计数据访问层(DAL)
是对数据库执行查询、更新、插入和删除操作的代码,是最接近数据库的代码,必须了解数据库的所有细节。
1.用提供程序模型设计模式来支持多种数据库
不能直接写DAL类,应当先写一个抽象基类(IDAL),其中定义(CRUD方法的签名),必要的话(实现一些辅助方法)。真正的数据访问代码是在继承于基类的次类(称为提供程序[provider])中[如SQLDAL]。通常专门支持某一类型的数据库[SQLServer数据库]。
2. 使用强类型的DataSet还是自定义实体(Entity)
DataSet/DataTable:
优点:智能感知/内置排序和过滤/IDE完美集成/binary formatter[二进制序列化]/支持不同并发策略
缺点:性能和扩展的局限性(要完整的DataTable,增加内存开销)/数据的表示形式模糊,数据库结构发生一点改变(字段变化),必须重建类型化的DataSet/添加自定义业务和验证逻辑很困难
自定义实体对象是一个类:用面向对象的方式将从数据库中获取的数据封装,使得数据库结构和其他细节抽象化。
第一种情况:实体类简单,只是与数据库的表(视图)一一对应,无增/删/改/查的方法。
第二种情况:还增加了对其父对象或子对象进行引用的其他属性,还要有操作数据的实例方法。(这种类称为域对象)
优点:更容易让UI人员使用,容易维护,只载入需要的数据(更快/更少内存),可自定义序列化方法,自定义验证逻辑。
在Web应用程序中,特别是在BLL和UI之间,作者更倾向于使用自定义对象而不是DataSet。
3.使用存储过程还是用SQL文本查询
在SQL文本查询中,只要使用带参数的SQL语句(避免SQL注入攻击)查询和存储过程(‘预编译’)的性能是差不多的。
存储过程更好地进行数据安全访问控制(可以给用户指定权限)---比如限制访问某些表的字段。不过,很少用到行级安全的时候。
存储过程通常包含了一批语句,调用时只需要传名称,而不是很多字符组成的SQL语句,可使网络通信量最小化。
存储过程提供了深一层的代码。在DAL层中可以没有SQL代码。方便对DAL部署和维护。
SqlCommand执行SQL文本查询的最大优势就是灵活。(UI中的高级查询和过滤功能/动态查询)
总结:通常情况使用存储过程来获取和处理数据库中的数据,除非对存储过程来说查询太动态和复杂,那就使用SQL文本查询。
4.数据访问类的基类
抽象类:从继承树的垂直方向给我们提供复用功能
接口: 从树的交叉方向给我们提供这些功能的替代(称为多态性)
只有很好地掌握了这些概念才能为系统构建出最好的体系结构。
所有抽象基类提供程序本身都是从一个基类(DataAccess)继承的。
DataAccess:提供一些属性,从web.config读取自定义配置进行封装,对DbCommand对象(SqlCommand、OledbCommand)的基本方法(ExecuteNonQuery、ExecuteReader和ExecuteScalar)进行封装。
设计业务逻辑层
在开始阶段,使用面向对象和强类型化的数据表示方式比用DataSet来传递数据需要更多的开发时间,需要更多有才能和经验的开发人员来进行设计,但在以后会得到更好的可维护性和更高的可靠性。
只要拥有了一个设计良好的、完全的、使用域对象的BLL层,开发用户界面将很容易,以前花费的时间在UI开发时得到弥补。
下面的代码片段展示了UI开发人员怎样获取一个填充了一些数据的Customer对象,对它的一些字段进行更新,并且保存到数据库。
Customer cust=Customer.GetCustomerByID(2);
cust.FirstName=”Marco”;cust.LastName=”Bellinaso”;
cust.Update();
使用UpdateCustomer静态方法会更简单一些,因为不需要创建类实例:
Customer.UpdateCustomer(2,”Marco”,”Bellinaso”);
- 如何避免对同样的、没改变的数据进行又一次查询?
- 如何对多个子操作组成的方法进行事务处理?
- 如何把需要关注的事件记录到日志中?
1.缓存数据以提高性能
aspnet_regsql.exe创建SQL依赖所需要的表、触发器和存储过程。
aspnet_regseq.exe –E –s .\sqlexpress –d aspnetdb –ed
注意事项:确保[SQL Server配置管理器中的SQL Server 服务开启]
图示参考:
运行isqlw.exe,登录进去后,看不见mydb.mdf,所以先附加一个映射的逻辑数据库名称,命令如下:
sp_attach_db DBlogicName,"F:\netStudy\三层体系\三层体系\App_Data\mydb.mdf",这样,再次刷新数据库,看到了DBlogicName 。
运行控制台命令:
aspnet_regsql.exe –E –S .\sqlexpress –d DBlogicName –ed
DBlogicName数据库启用了缓存依赖,生成了相应的表和存储过程:
下一步是对一个特点的表添加支持,意味为该表创建一个触发器,并在AspNet_CacheTablesForChangeNotification表中新增一条记录:
aspnet_regsql.exe –E –S .\sqlexpress –d DBlogicName –t Customers –et
最后不要忘记分离数据库: sp_detach_db "DBlogicName”
最后在web.config中进行轮询设置。
配置好后,可以写代码来进行数据缓存了。
System.Web.Caching.SqlCacheDependency dep=new SqlCacheDependency(“SiteDB-cache”,"Customers”);
Cache.Insert(“Customers”,Customer.GetCustomers(),dep);
这种缓存机制还可以用于ASP.NET的输出缓存功能(Output Caching) 功能,即将页面生成的HTML缓存起来。
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="三层体系._Default" %><%@ OutputCache Duration="3600" VaryByParam="none" SqlDependency="SiteDB-Cache:customers" %>
使用该指令,网页的输出将被缓存最多一个小时,如果Customers表中的数据发生改变,这时间更短。
网页中看效果:
protected void Button1_Click(object sender, EventArgs e) { GridView1.DataBind(); }
等待几分钟后,点击按钮,可以看到页面【时间】没有变化,已经正确缓存了。
我们来改动数据,再点击按钮看看效果:
可以看到,数据改动后,重新缓存了。缺陷:当该表的任何数据改变,都会使缓存失效。为表级别的缓存依赖跟踪。
但SQL Server2005增加了行级别的缓存依赖跟踪。
SQL Server2005特定的SQL 依赖支持
private string connStr = ConfigurationManager.ConnectionStrings["SiteDB"].ConnectionString; protected void Page_Load(object sender, EventArgs e) { connStr = connStr.Replace("|DataDirectory|", Server.MapPath("~/app_data")); SqlDependency.Start(connStr);//应放在global.asax中的Application_Start全局事件处理程序中 using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); SqlCommand cmd = new SqlCommand("select name from dbo.customers", conn); SqlDependency dep = new SqlDependency(cmd); dep.OnChange += new OnChangeEventHandler(dep_OnChange); SqlDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { Trace.Write(reader["name"].ToString()); Response.Write(reader["name"].ToString() + ""); } } } void dep_OnChange(object sender, SqlNotificationEventArgs e) { StreamWriter sw= File.AppendText("c:\\changeData.txt"); sw.Write("\n Customers表数据已经更改,请重新获取最新数据!" + DateTime.Now.ToString()); sw.Close(); }
这种技术的限制很严格 :
不能用“*”,必须列出所有字段,引用表需要全名(dbo.customers)/不能用聚合函数/不能使用排序和windowing函数/不能用视图或临时表/查询不能返回text、next或image(blob类型)的字段/不能用DISTINCT、HAVING、CONTAINS和FREETEXT。/不能包括子查询、join/存储过程不能用SET NOCOUNT ON语句
事务管理
新的System.Transactions名称空间
在DAL中由于共享同一个连接,那么将会创建一个轻量级的事务。通常事务在BLL运行,并且必须封装调用几个其它的方法,那些方法可能在内部创建了单独的针对不同数据库的连接。可能会在底层创建了一个分布式的COM+事务。
using (TransactionScope scope = new TransactionScope()) { MyBizObject1 obj1 = new MyBizObject1(); obj1.DoSomething(); MyBizObject2 obj2 = new MyBizObject2(); obj2.DoSomethingElse(); scope.Complete(); }
作者不推荐在共享的Web主机上使用COM+、SWC或者System.Transcation,因为不能控制服务器。
配置健康监视系统
该系统以提供程序模型设计模式为基础:意味从一个通用的抽象类继承出几个类,并提供具体的实现。内置的提供程序能把日志保存到SQL Sever数据库、Windows Event Log、WMI Log 或电子邮件中。也能通过继承System.Web.Management.WebEventProvider基础提供程序类来创建自己的日志提供程序。
存储在web.config文件中的配置格式如下:(可为日志事件定义规则、注册自定义事件、注册并选择用于日志事件的提供程序类)
... --手动注册需要记入日志的内置类和自定义类。为注册的事件类指定一个名称... -- 注册提供程序... --设置要将事件记入日志、该事件使用什么提供程序。以及多长时间记录一次。... --可被运用到多个规则中... --可能的应用于提供程序的缓冲信息
为SQL Server提供程序建立数据库
aspnet_regsql.exe –E –S .\sqlexpress –d aspnetdb –A w
数据通过aspnet_WebEvent_LogEvent存储过程保存在一个名为aspnet_WebEvent_Events的表中。
所有业务类的基类
许多业务对象需要访问很多共享信息,如当前用户名、IP地址以及对当期环境中Cache对象的应用。这些信息可以放在一个基类中,让其他所有域对象从它继承。该基类还包含一些辅助方法,如通过替换特殊字符来对HTML输入进行编码,以及清除缓存中的项目。
保持基类短小,让它只包含子类所需的通用功能是个好的设计习惯,以后如何需要一个通用的属性或方法,在架构中拥有一个基类会很方便。
用户界面(省略)