Chapter 3: 规划结构
一、Problem问题
我们必须解决以下公共的设计问题:
- 多层设计:从商业逻辑层和表示层(用户界面)分离数据访问层,这样网站更易维护和升级。
- 独立的数据访问结构,能支持不同的数据存储类型 — 不需要改变商业对象层当数据改变时。同样改变商业对象或表示层也不需改变另外的层,这叫作decoupling the tiers.
- 设计商业对象结构以面向对象的方式通过数据访问层来访问。Design the business object architecture to expose the data retrieved by the data access layer in an object-oriented format. This is the process of mapping relational data to OOP classes.
- 支持缓冲caching商业对象
- 操作和日志异常,包括其它重要事件比如删除记录,以帮助分析出错的问题,提供追踪记录
- 存储网站和模块的配置在一个地方,能容易的读写它,提供一个帮助类去简单的访问这些设置
- 把大量的数据(从商业逻辑层获得)绑定到界面控件,最小化界面层的工作量。把管理数据的规则放在商业逻辑层而不是UI层。理想情况是,UI层集中于数据的显示,商业逻辑层操作数据、添加商业规则;数据层只提供存储功能。
二、Design
1、设计一个分层结构
- 数据存储: 可以是关联数据库,一个XML文件,一个text文件,或者其它.
- DAL数据访问层: 获得和操作存储的原始数据的代码
- BLL商业逻辑层: 代码。取出从数据访问层获得的数据,把它以直接的方式递交绐客户端,隐藏底层的细节比如数据的存据格式,添加所有的确认逻辑(确信输入是安全和一致的)。
- 表现层(用户界面): 代码。定义了用户将在屏幕上看到的类容,包括格式化的数据和系统导航菜单。这一层被设计到Web界面或者Form窗口界面.
代码文件一般放在App_Code目录下,按层和类容再分子目录。对更大型,安全要求更高的网站,可分成多个项目的DAL代码单独放到一台服务器里。
2、选择一个数据源Choosing a Data Store
SQL2005Express版只支持一个CPU,1GB RAM,最大4GB数据,高级的性能如数据分区,数据镜像,通知服务和完全的文本搜索也不支持.
3、设计数据访问层
数据访问层(DAL)是这种代码:对数据执行查询,更新、插入和删除数据,它是最靠近数据库的代码,它必须知道数据库的所有细节,例如:表结构,字段名,存储进程,视图等等。你将保持数据操作代码分离于你的网页。
3.1、采用应用者Provider模式设计方案去支持多种数据源
代替直接的写DAL类,你将首先写一个抽象的基类,定义了公共的接口类,如果需要,执行一些帮助方法。实时的数据访问代码在它的派生类中定义,重载父类的抽象方法。这些类被称为供应者,通常指定为访问某种类型的数据库。当你执行他们时,你不必怛心任何其它类数据库的兼容问题。不同的供应者放在不同的目录(如 App_Code/DAL/SqlClient),名字空间(如:MB.TheBeerHouse.DAL.SqlClient)
Figure 3-1
3.2、永恒的问题:使用数据集DataSet还是自定义封装项?
3.2.1不足
DataSet缺乏对数据库对象的封装,很难添加商业和确认规则一些最通用的工具。我的观点是:即使我看到DataSets被用在一些可升级的项目(甚至有上万个用户),自定义对象仍然是更灵活和优雅的,我一般在Web 程序更喜欢用它们,特别在BLL和UI层间。但用自定义对象需要你多花些时间去设计BLL层的代码。
.NET developers 是NHibernate (
nhibernate.sourceforge.net [open source]),
EntityBroker (
www.thona-consulting.com/content/products/entitybroker.aspx [free community version and commercial versions available]),
LLBLGen Pro (
www.llblgen.com[commercial]).
一个为中小型系统的简单工具
Paul Wilson's ORMapper (
www.ormapper.net).
3.3、使用存储进程还是SQL文本查询?
我经常用存储进程支获得和处理数据库数据,当查询对一个存储进程太频繁和复杂时,我动态的建立SQL查询文本。
3.4、所有数据访问类的基类。
Figure 3-2
T在
DataAccess
基类的ExecuteNonQuery 函数是一个特别的助手方法,能够用来把存储进程的名字或者
SQL
查询文本中表的名字翻译成网站使用的特定数据库的固有名称(本书不包括这些内容)。in
The DataAccess Base DAL Class
DataAccess 类 (位于 /App_Code/DAL/DataAccess.cs) 只包含了一些属性,比如连接字符串,EnableCaching,CachingDuration and Cache, 和ExecuteReader, ExecuteScalar 以及ExecuteNonQuery 封装方法.注意除了Cache属性直接返回当前的Cache对象外,其它属性未直接包含,回为派生的不同Dal类可能有不同的值。我不计划用Cache在DAL,因为我更喜欢执行Caching在BLL,所以它工作时可能不考虑哪个DAL提供者在被用,但我仍放Cachin-relate属性在DAL的基类。
4、设计商业逻辑层
The DAL discussed in the
previous section is made up of a number of classes that
上面的DAL由一些获取数据的类构成,通过运行存储进程返回一个自定义的类集合(封装了获得的数据字段),获得的数据仍然是原始数据,因为这些实体类并不对数据做任何操作。BLL消费这些数据,并把它们交到UI层, BLL也添加校验逻辑和适当的道具,使一些道具私有或只读(在 DAL和BLL层中它们是可写的), 添加实例和静态方法去删除,编辑,插入,获得数据。为一个领域对象如叫雇员的表示这个雇员的内容。:
Customer cust = Customer.GetCustomerByID(3);
cust.FirstName = "Marco";
cust.LastName = "Bellinaso";
cust.Update();
Figure 3-3
这是几个你需要经常考虑的问题:
- 我是否能或者怎样才能避免每几秒就查询同样的不改变的数据
- 我怎样能用事务的方式提交处理数据。
- H我怎样能记录异常事件和用户感兴趣的事件?
下面是解决上述问题的建议.
4.1、为更好的性能缓存数据
4.1.1SQL Server 7+ Support for Table-Level SQL Dependencies
4.1.2 SQL Server 2005-Specific SQL Dependency Support
Important重点:
注意,为了这代码能工作,你必须首先使查询提示为能够,在你的客户程序调用
SqlDependency.Start 方法一次, 在Web程序中是
在 global.asax文件中的Application_Start 全局事件. 对WinForms程序可以在Main entry-point方法,或者主form的
Form_Load 事件.
下面是许多重要的约束:
- 在SELECT 查询中你不能用 * 通配符,你必须指明所有的字段,这是一个好习惯。
- 你必须引用表的完整名字,例如: dbo.Customers.只写 Customers是不够的。
- 查询不能用聚合函数,比如 COUNT, SUM, AVG, MIN, MAX, 等等.
- 查询不能用顺序各窗口函数,比如ROW_NUMBER() 函数。
- 查询中你不能引用视图或者临时表.
- 查询不能返回下述字段类型 text, ntext, or image (blob types). .
- 你不能用 DISTINCT, HAVING, CONTAINS and FREETEXT.
- 查询不能包括子查询 subqueries, outer-joins or self-joins. 这是一个最大的限制。
- 上述限制不论是在从客户端直接运行的SQL文本命令还是在存储进程中都有效。对存储进程则有更多的限制:你不能用SET NOCOUNT ON 语句(经常被用于不需要计数的情况) .
4.1.3 选择一个适合你的需要的Caching策略
4.2. 事务管理Transaction Management
4.2.1ADO.NET 事务
下面为例子:
using (SqlConnection cn = new SqlConnection(connString))
{
cn.Open();
SqlTransaction tran = cn.BeginTransaction();
try
{
SqlCommand cmd1 = new SqlCommand(cmdText1, cn, tran);
cmd1.ExecuteNonQuery();
SqlCommand cmd2 = new SqlCommand(cmdText2, cn, tran);
cmd2.ExecuteNonQuery();
tran.Commit();
}
catch(Exception e)
{
tran.Rollback();
}
}
这个事务处理的代码有着下面的制约:
- 你必须从DAL中使用它们.
- 这个事务一定要一个单一的连接,这意味着它不能跨多个数据库T.
- 你必须执行所有的命令在同一个连接。
4.2.2、COM+ and SWC 事务
用COM+可以解决上面的问题,下面是一个简单的事务类去配置自动的事务 :
[Transaction(TransactionOption.RequiresNew)]
public class SampleBO : ServicedComponent
{
[AutoComplete]
public void UpdateDate()
{
MyBizObject1 obj1 = new MyBizObject1();
obj1.DoSomething();
MyBizObject2 obj2 = new MyBizObject2();
obj2.DoSomethingElse();
}
}
Note
一般COM+的使用,需要有管理员权限去注册产生的组件.这对共用一个服务器发布网页的小网站是很难做到的。幸运的是,你的发布网站的主机如果是Windows Server 2003, 你能够用COM+ 1.5 的新属性叫做Services Without Components (SWC).它也能工作在Windows XP, 你不能发布到一个客户版的 Windows . 下面是例子:
// configure and start the transaction
ServiceConfig svcConfig = new ServiceConfig();
svcConfig.TrackingEnabled = true;
svcConfig.TrackingAppName = "TheBeerHouse";
svcConfig.TrackingComponentName = "MB.TheBeerHouse.BLL.SampleBO";
svcConfig.Transaction = TransactionOption.RequiresNew;
svcConfig.IsolationLevel = TransactionIsolationLevel.ReadCommitted;
ServiceDomain.Enter(svcConfig);
try
{
MyBizObject1 obj1 = new MyBizObject1();
obj1.DoSomething();
MyBizObject2 obj2 = new MyBizObject2();
obj2.DoSomethingElse();
ContextUtil.SetComplete();
}
catch (Exception e)
{
// rollback the transaction
ContextUtil.SetAbort();
}
finally
{
ServiceDomain.Leave();
}
4.2.3、新的系统事务名字空间
比COM+更好的: 新系统事务名字空间 。这里介绍两个新的事务管理类: Lightweight Transaction Manager 和OleTx Transaction Manager. 下面的例子开始一个事务和运行几个不同命令:
using(TransactionScope scope = new TransactionScope())
{
using (SqlConnection cn = new SqlConnection(connString))
{
cn.Open();
SqlCommand cmd1 = new SqlCommand(cmdText1, cn);
cmd1.ExecuteNonQuery();
SqlCommand cmd2 = new SqlCommand(cmdText2, cn);
cmd2.ExecuteNonQuery();
}
scope.Complete();
}
例2:
using(TransactionScope scope = new TransactionScope())
{
MyBizObject1 obj1 = new MyBizObject1();
obj1.DoSomething();
MyBizObject2 obj2 = new MyBizObject2();
obj2.DoSomethingElse();
scope.Complete();
}
4.3、健康监视和异常处理Health Monitoring and Exception Handling
使用customErrors段web.config 如下:
记录错误在页面基类里:
public class BasePage : System.Web.UI.Page
{
protected override void OnError(EventArgs e)
{
// log last exception, retrieved by Server.GetLastError()
}
...
}
或者你能处理这同样的Application_Error事件在Global.asax.cs 文件:
void Application_Error(Object sender, EventArgs e)
{
// log last exception, retrieved by Server.GetLastError()
}
ASP.NET 2.0 的 the
instrumentation and health monitoring system提供了更好的性能,你可用它建立你自定义事件记录比如删除记录。下面是监视系统记录到的样例:
Event code: 3005
Event message: An unhandled exception has occurred.
Event time: 10/10/2005 3:52:47 PM
Event time (UTC): 10/10/2005 10:52:47 PM
Event ID: 82cfbe7544f54a10abfb31fcabc1d466
nt name: VSNETBETA2/Marco
Exception information:
Exception type: DivideByZeroException
Exception message: Attempted to divide by zero.
Request information:
Request URL: http://localhost:14376/TheBeerHouse/Default.aspx
Request path: /TheBeerHouse/Default.aspx
User host address: 127.0.0.1
User: SampleEditor
Is authenticated: True
Authentication Type: Forms
at System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr fp, Object o,
Object t, EventArgs e)
at System.Web.Util.CalliEventHandlerDelegateProxy.Callback(Object sender,
4.3.1、配置健康监视系统
下面为事件类或自定义类:
- WebBaseEvent: 被所有其它事件继承的基事件类
- WebManagementEvent:所有程序事件的基类,包括请求错误,存活时间事件(程序开始和结束)和审计事件
- WebHeartbeatEvent: 一个被ASP.NET引擎周期调用的事件,包含程序当时的分健康表现:线程激活数,对列和执行请求,内存使用
- WebRequestEvent: T涉及到Web请求的所有事件的基类,比如失败的交互脚本确认,超出最大传递长度,所有其它的停止错误。
- WebAuditEvent: 成功或失败的身份确认事件 page's ViewState, cookies, 等.
- WebBaseErrorEvent: The base class for all error events, including exceptions and request/infrastructure errors
- WebErrorEvent: 基础错误事件,比如编辑,语法和配置
下面是构造供给类 (all located under the System.Web.Management):
- EventLogWebEventProvider: Logs events to the Windows Event Log
- SqlWebEventProvider: Logs events to a SQL Server (7, 2000 or 2005) database, as long as they have the expected tables and stored procedures (explained below)
- TraceWebEventProvider: Sends events to the current trace listener collection (typically the tracing is saved in memory to be displayed by trace.axd, or on the page's output itself, but additional listeners can be added to save information on a file or other media)
- WmiWebEventProvider: Forwards the web events to the Windows Management Instrumentation (WMI) subsystems
- SimpleMailWebEventProvider: Sends the web event's information by e-mail to the specified administrator
- TemplatedMailWebEventProvider: Like SimpleMailWebEventProvider, but with an additional property that points to a text file to be used as template for the mail, so that it can be completely customized and translated.
存储在Web.config文件里的配置格式如下:
下面是记录所有 built-in 事件:
下面例子注册了 providers for SQL Server, the Event Log, and the WMI subsystem:
maxEventDetailsLength="1073741823"
buffer="false" bufferMode="Notification" />
4.3.
2
、为
SQL
服务提供者设置数据库
这个数据存储在 aspnet_WebEvent_Events表中
4.3.3、为所有商业类的基类A Base Class for All Business Classes
4.4、存储连接字符串和其它设置
4.4.1(a)下例是连接字符串的使用,在web.config:
http://schemas.microsoft.com/.NetConfiguration/v2.0">
connectionString="Data Source=./SQLExpress;
Integrated Security=True;User Instance=True;
AttachDBFilename=|DataDirectory|TheBeerHouse.mdf" />
提醒: |DataDirectory| 占位符代表网站的App_Data 目录.
连接字符串会被许多其它的配置项引用 — 例如:健康监视系统的SqlWebEventProvider, 或者SQL-dependency 缓存设置. 缺省情况下,所有的这些项的connectionStringName设置成 LocalSqlServer, 是去调用SQL服务器自带的ASPNETDB.MDF数据库,这在/WINDOWS/Microsoft.NET/Framework/v2.0.50727/CONFIG/machine.config进行了缺省配置。如果想用自已定义的库,你可以先remove这个名字,再添加回你想要的内容。
4.4.2
(b)
、如果你的服务器只有这一个网站你可以去修改它 (在machine.config):
connectionString="Data Source=./SQLExpress;
Integrated Security=True;User Instance=True;
AttachDBFilename=|DataDirectory|TheBeerHouse.mdf" />
4.4.3
(c)、还有好的方式去从代码获得连接字符串,有一个新类System.Web.Configuration.WebConfigurationManager,它有一个连接字符串字典属性,可以通过名字去获得联接字符串,如下:(方括号用来检索到字典):
string connString = WebConfigurationManager.ConnectionStrings[
"LocalSqlServer"].ConnectionString;
这个类也有AppSettings字典属性,让你读存储在
段里的内容。
对于自定义的配置内容可以从System.Configuration.ConfigurationSection派生一个类,类定义的属性和web.config文件中设置对应,然后可以用这个类方便的访问web.config中的内容。对于这个类中的元素嵌套,你需要建立一个新类(从 ConfigurationElement类派生--而不是 ConfigurationProperty), 然后再定义ConfigurationProperty 属性.下面是例子:
public class SiteSection : ConfigurationSection
{
[ConfigurationProperty("title", DefaultValue="Sample Title")]
public string Title
{
get { return (string)base["title"]; }
set { base["title"] = value; }
}
[ConfigurationProperty("homePage", IsRequired=true)]
public HomePageElement HomePage
{
get { return (HomePageElement)base["homePage"]; }
}
}
public class HomePageElement : ConfigurationElement
{
[ConfigurationProperty("showAdBanners", DefaultValue="true")]
public bool ShowAdBanners
{
get { return (bool)base["showAdBanners "]; }
set { base["showAdBanners "] = value; }
}
}
在 web.config 文件中定义段名 "site", 如下:
http://schemas.microsoft.com/.NetConfiguration/v2.0">
type="Company.Project.SiteSection, __code"/>
//
注意Company.Project.SiteSection须正确
下面是读取配置的代码:
SiteSection site = (SiteSection)WebConfigurationManager.GetSection("site");
string title = site.Title;
bool showAdBanners = site.HomePage.ShowAdBanners;
注意:
SiteSection
类的新实例,首先从类定义中获取初始的缺省值,调用
GetSection()
函数后从
Web.config
中读取对应值覆盖原有值。
5、用户界面User Interface
5.1、新的绑定属性和数据绑定控件
- GridView: DataGrid控件的增强版, 有新的列类型,能自动显示和处理图像和选择框,还可显示简单的原文数据,命令按钮,超链接,基于模板的数据。通过写非常少的代码(或者不用写代码)还支持分页,两路排序,编辑,删除 !
- DetailsView: 显示单个记录的细节在一个设计的表格,用两列:名字和值。这控件也支持编辑和添加新记录.
- FormView: 和DetailsView类似,但它是完全基于模板(有许多灵活的格式),也支持编辑,插入和分页
SqlDataSource:指定一个SELECT 语句去获得要显示的数据,
INSERT, UPDATE 和DELETE语句去操作数据库,也能用存储进程代替SQL文本,
它并不专为SQL服务器使用,Oracle一样可以用。
ObjectDataSource: 调用方法在一个商业类去获得数据和操作它。
XmlDataSource, 引用一个XML文件,充许你去指定 XPath查询去获得数据。
5.2、SqlDataSource 和GridView 控件
Using the FormsView Control
The ObjectDataSource Control