.Net Petshop详解(一): petshop概览和准备工作
Youther
前言
Microsoft公司为了配合推出.NET战略,不仅发布了一系列的开发工具和开发平台,同时还推出了.NET平台上的示范应用.其中的.Net Petshop就是极好的一例.这个电子商务的开发应用是为了应对J2EE平台而出炉的,在J2EE平台上Sun公司也有一个完整的实例----J2EE Petstore,它体现J2EE的BluePrint。二者解决的问题是一样的,功能也大同小异,界面也是如出一辙。自然是公说公有礼,婆说婆有礼了,我们姑且不管双方怎么声称自己的东西好,但是从中也能足见.NET和J2EE平台上的完美杰作是怎么样的一个实现。在这里,本人无意去争辩问题的胜负,也不打算去探讨J2EE是怎么样的解决方案和实现,只是作为纯粹的技术角度来管窥.NET平台上的.Net Petshop是如何一套的解决方案和实现的。我想,最好的学习,莫过于钻研这样的一套大的经典的系统,从设计的思想到最好的编码,毫无疑问对于我们将是大有裨益的。在这里,我将陆续的系列中逐步与大家一起探讨详解.Net Petshop的方方面面。希望大家多多指正!
.NET Pet Shop解决问题的描述
.NET Pet Shop是一个电子商务的实例,是在Microsoft的.NET平台上的一个具体实现。这个系统包含了B2C和B2B的实现。在这里我主要将的是B2C实现的部分,B2B在.NET Pet Shop里面不是重点要解决的问题。
.NET Pet Shop是一个在线的宠物购物系统,用户可以通过各种能够连接到internet的终端(包括移动终端)进行在线购物(具体界面见图一)。在这里,我将列出它将要实现的功能,也就是需求了:
(1) 用户帐号的管理功能:包括帐号创建,帐号登录,帐号维护;
(2) 产品浏览功能:类别浏览,具体产品浏览,详细信息,库存信息等等;
(3) 用户购物功能:添加购物,计算总价,下订单等等。
图一 .NET Pet Shop系统的登录主界面
.NET Pet Shop的逻辑结构
.NET Pet Shop按照三层的分层模型来部署整个系统。.NET Pet Shop分为数据展示层,中间层,数据层。这三层被清晰的分割为分布式应用的三个方面。其中数据展示层主要是完成界面和与最终用户交互的功能,在应用程序里面是一些aspx的页面和代码;中间层用于封装商业逻辑和规则,在应用程序里面被封装为.NET组件;数据访问通过中间层中的数据访问组件与SQL Server Provider交互,所有的数据获取依靠存储过程来进行,而不是通过SQL语句。图二是参考petshop的白皮书说明.NET Pet Shop的物理部署结构。
图二 .NET Pet Shop部署结构
.NET Pet Shop的开发环境支持
Microsoft的.NET战略是如此的宏大,同样的.NET Pet Shop需要的开发环境同样是很壮观的。以下是我调试.NET Pet Shop的环境,有些东西是可以替换的,有些不是必须的。大家可以根据自己的情况调整。
(1) 操作系统win2000 server,web服务器IIS5.0,数据库服务器SQL Server 2000;
(2) 开发工具Visual Studio.NET企业设计版。
在这里如果大家想很快的知道.NET Pet Shop 的UML模型,可以利用Visio来进行反向工程,就可以自动生成静态的类图(但是不可能生成用例图和序列图等等的)。同时Visio也可以对已经有的petshop数据库进行反向工程,生成数据库模型,从而对理解.NET Pet Shop大有帮助,也能够管窥一点微软的建模方法和设计过程。
.NET Pet Shop系统的安装
.NET Pet Shop的示例程序代码和白皮书可以在Microsoft的msdn网站上下载到,也可以到http://www.gotdotnet.com/compare去下载。
在petshop的文件夹下有database、petshopdoc和web以及webservice几个文件夹。Database是数据库的安装脚本,将你的数据库的sa密码设置为空即可直接运行这些脚本安装。Petshopdoc是英文的白皮书,后面的比较有参考价值了。Web是我们将要讨论的大部分程序代码。Webservice是用于发布的web服务。
下一篇我将就.NET Pet Shop的三层应用程序结构作详细的剖析。
.Net Petshop详解(二): petshop三层结构之DataTier
交互的三层综述
在前面的文章中,我们丛整个系统的角度了解了petshop的三层部署结构物理上的一个概貌,没有从应用程序的角度来说明是如何划分的,也不清楚数据展示层,中间层,数据层是如何交互的。为了更好的说明这个三层的结构以及每一层的具体细节问题,也让大家对整个应用有一个walkthrough,在这里我将说明从系统的角度来看(见参考图1),顾客在购物的过程中,系统的三层是如何的工作以及如何的交互。这个东西有点象UML里面的实例情景故事,对需求和分析的进一步是大有帮助的。
图1 .NET Petshop 架构
从这个情景中,我们可以很清晰的了解到Internet Customer在购物时系统的运作情形。首先通过数据展示层的用户交互界面Cart.aspx页面输入顾客购买的商品以及数量,然后这些购物清单由后台的应用程序(cart.aspx.cs)做相应的预处理(如安全验证,校验,数据的格式化等等),接着调用中间件(在程序结构里面就是一些.NET Assembly,封装了购物的商业逻辑),最后通过数据访问接口来更新数据库里面的数据(数据层)。
数据层(Database)
.NET Petshop的数据库并不是十分的庞大,总共有12个用户表和23个存储过程。Petshop数 据库里面存储的是用户数据,帐号数据,产品数据,用户配置数据,订单数据,库存数据以及供应商的数据。应用程序访问数据库的数据并不是直接的与数据库表打 交道,而是通过存储过程的运行来获取所需要的数据。这样的设计有一个好处就是,避免了频繁的表操作,而通过运行在服务器端的存储过程可以极大的提高运作效 率和提升访问数据的速度,同时也很好的屏蔽了数据库表的逻辑,使得数据库访问变成了数据库提供的服务访问。当然,也有人指责说这些存储过程迁移性是值得怀 疑的。
下面我将通过表格把这些数据库的基本表列出来(见表1),并一一做说明,希望对大家深入的理解有帮助。
表名称 | 备注信息 |
Account | 基本用户信息。 |
BannerData | 存储的是系统界面的banner图片的设置信息。 |
Category | 宠物的类别目录表(比如鱼类,狗类等等)。 |
Inventory | 宠物产品的存货信息。 |
Item | 单个产品的详细信息。 |
LineItem | 订单的每一项的详细信息,包括产品名称和数量,价格等。 |
Orders | 用户购物的订单,一个订单可以包括多项LineItem |
OrderStatus | 订单的状态 |
Product | 宠物的产品列表,一条Product可能包括多个Item |
Profile | 用户配置表,用于记录他们的favorites。 |
Signon | 用户帐号登陆表,因为常用,故从Account独立出来。 |
Supplier | 供应商信息。 |
通过上表我们知道了petshop数据库的数据库表的一些信息,但是这些表之间到底是一种什么样的关系呢?关心数据库建模和设计的人可能对这个问题比较感兴趣。在这里,我将给出图表(图2)说明这些数据库表之间的关系。图2中列出了这些表之间的关系和主键信息。
图2:petshop数据库物理设计模式
技巧:其实对于从事过数据库建模和设计的人都知道,得到上面的数据库模型图形并不是一件很困难的事情。正如我们前面提到的一样,我们可以用Visio做工程反转就可以得到上面这么美观的设计模型图了。同时,我们也可以在模型图中做修改设计,可以马上应用到你的物理数据库,使其保持同步。
说完了基本的数据库表,接下来我们看看存储过程。用微软的话说,只有设计称存储过程,才算是”cleaner separation of code from the middle-tier”,我个人觉得这样做是很好的。同样的,我把它列在一个表格里(表2)。
存储过程名称 | 备注信息 |
upAccountAdd | 增加一个帐号。 |
upAccountGetAddress | 获取用户的地址,主要用于下订单时注册地址与送货地址不一。 |
upAccountGetDetails | 获取帐号的详细信息。 |
upAccountLogin | 用户登陆验证。 |
upAccountUpdate | 更新用户帐号。 |
upCategoryGetList | 获取某个类别的产品列表。 |
upInventoryAdd | 增加指定的项到存活信息。 |
upInventoryGetList | 获取存货列表。 |
upItemAdd | 增加一项产品。 |
upItemGetDetails | 获取指定产品项的详细信息。 |
upItemGetList | 获取某一特定类别的产品的具体项目列表。 |
upItemGetList_ListByPage | 功能与上同,但是分页获取数据。 |
upOrdersAdd | 增加一项订单。 |
upOrdersGet | 获取某一订单的信息。 |
upOrdersGetDetails | 获取某一订单的详细信息。 |
upOrderStatusGet | 获取订单的状态。 |
upProductAdd | 增加一类别产品。 |
upProductGetList | 返回某类产品的列表。 |
upProductGetList_ListByPage | 与上同,但是分页获取结果数据。。 |
upProductSearch | 产品搜索。 |
upProductSearch_ListByPage | 与上同,但是翻页获取结果数据。 |
upProfileGetBannerOption | Banner的配置信息。 |
upProfileGetListOption | 获取用户配置信息。 |
表2:petshop存储过程列表
在这些存储过程里面使用了SQL Server2000的OpenXML的特性来代替传统的行集结果,使用在非常频繁的数据访问操作,可以减轻系统的负担。
好了,petshop的Datatier就说到这里了,在后面的文章我会就一个数据访问的实例再次解说存储过程的执行。接下来我将就中间层做介绍了。
.NET Petshop详解(三):petshop三层结构之MiddleTire
通过前面的文章,我们对.NET Petshop的整个结构有了一个大致的了解,也清楚的知道了数据库的设计模式和实现的细节,尤其值得一提的是通过存储过程访问数据库。在接下来的这篇文章里,我将和大家一起来探究一下.NET Petshop的中间层。
根据三层结构的设计原则,中间层封装的是业务逻辑和规则,在这个网络宠物商店的例子中,购物处理,订单处理,帐号管理,产品查询等等都是具体的业务逻辑,至于与用户交互并不是中间层要处理的问题。它处理是与具体的用户界面和交互无关,而仅仅是核心的商业规则和逻辑。.NET Petshop的中间层业务逻辑被封装为一个.NET 组件,它的命名空间为Pet Shop.Components(编译后在bin的文件夹里面有一个petshop.dll的文件)。图1是.NET Petshop解决方案中间层的类视图和文件视图。
图1:.NET Petshop解决方案中间层的类视图和文件视图
接下来,我们模拟顾客到百货超市采购日常用品的过程来说明运作的流程以及抽象出重要的概念(实际上User case,我们在领域分析的时候会这么做,并且是很重要的一步,从这里可以初步的发现在我们实施的系统中将要涉及到的逻辑实体,进而可以为数据库建模设计以及类设计提供参考)。
购物用例的业务分析:
1 客户有购买商品的意愿;
2 客户到登陆管理员处登记,且成功登记;
3 在登记处推一个购物车;
4 在超市内查找所购商品类别存放的货架;
5 在具体的货架上查找某一具体品牌的商品;
6 将符合意愿的商品放入自己的购物车;
7 重复4-6;
8 购物完毕;
9 到付款处计算总价格并付款;
10 打印购物清单;
11 退还购物车;
12 取走购物,购物完毕;
备注:在这个用例中,我们做了一些前提和假设,为的是方便.NET Petshop的分析,比如说在实际生活中根本就不需要第二步。
通过这个用例的分析,我们至少可以抽象出一下几个重要概念,并且能在应用程序里面找到对应的类:客户对应Customer、商品对应Product、购物车对应ShoppingCart、商品类别对应Category、具体商品对应Item、清单对应Order。
正如我前面说过的,这几个概念对于我们的业务建模和系统建模是非常有用的。正是通过这样的分析,在.NET Petshop的业务逻辑里面共有9个核心类和5个轻量级的数据结构类。同样的方式,我在这里列出这些类,并加以说明(见表1)。
类名称 | 说明 |
BasketItem | 代表购物车ShoppingCart里的一项购物商品。 |
Customer | 用于帐号管理和登陆验证。 |
CustomerDetails | 用户帐号的详细信息。 |
CustomerAddress | 用户帐号的地址信息。 |
Error | 用于登陆出错的帮助功能。 |
Item | 代表某类产品中的具体一项商品。 |
ItemResults | 搜索Item的结果集。 |
Order | 购物完毕后的购物清单和订单。 |
Product | 大类别里面的某类产品。 |
ProductResults | 搜索产品的结果集。 |
Profile | 用户的配置。 |
ShoppingCart | 购物车,用于购物的整个过程,直到下订单。 |
Database | 通过ADO.NET访问数据库,封装了具体的访问方法。 |
SearchResults | 模糊搜索的结果集。 |
表1:.NET Petshop中间层的类
CustomerAddress, CustomerDetails, ItemResults, ProductResults, and SearchResults这几个轻量级的数据结构类为在数据层和展示层之间提供了一种松散的数据绑定调用。这些类都被设计为有公开的属性,ASP.NET 的web页面可以通过这些属性访问数据。下面这段类的代码说明了这5个类是如何暴露自己的公开属性供展示层使用的。
public class ProductResults
{
private string m_productid;
private string m_name;
public string productid {
get { return m_productid; }
set { m_productid = value; }
}
public string name {
get { return m_name; }
set { m_name = value; }
}
}
在.NET Petshop详解(二)中我们就说过数据库的访问是通过存储来进行的,我们看看下面这部分代码就知道了:
public string Login(string userName, string password) {
string customerID;
// params to stored proc
Database data = new Database();
SqlParameter[] prams = {
data.MakeInParam("@username", SqlDbType.VarChar, 25, password),
data.MakeInParam("@password", SqlDbType.VarChar, 25, userName),
data.MakeOutParam("@CustomerID", SqlDbType.VarChar, 25)
};
// create data object and params
data.RunProc("upAccountLogin", prams); // run the stored procedure
customerID = (string) prams[2].Value; // get the output param value
// if the customer id is an empty string, then the login failed
if (customerID == string.Empty)
return null;
else
return customerID;
}
这段代码是Customer类的Login方法,它是通过将用户输入的用户名userName和密码password做为输入参数传递给存储过程upAccountLogin的,这个存储过程完成在Sigon用户帐号表里面查找该用户是否合法,最后返回一个字符串的用户ID值。在这里没有使用SQL查询语句,很好的分离了逻辑。具体的数据库访问是通过Database来完成的,我们将在后面的文章中继续探讨它的运作。
ShoppingCart是比较有意思的一个类,也是很重要的一个类。它是与状态有关的一个类,在.NET Petshop里面,它的状态是通过ASP.NET Session state来管理的,关于其进一步的细节留待后面讨论。
.NET Petshop的中间层的探讨先到此,我在这里只是抛砖引玉,很多的东西要深入代码才可以搞的更加清楚。欢迎大家继续与我一起关注下一篇.NET Petshop的数据展示层。
.NET Petshop详解(四):petshop三层结构之PresentationTier
在前面的文章中,我们已经就.NET Petshop的数据层和中间的业务逻辑层作了说明,接下来的文章中,我们将就数据展示层作探究。与前面的两层有着很大的差别的是,.NET Petshop的展示层用了很多Microsoft最新的web Forms技术即Asp.NET。因此,在解说展示层之前,我想就Asp.NET在.NET Petshop的开发中使用到的非常重要的特性做一些说明,以示区别:
(1) Asp.NET代码不再是解释型代码,可以经由JIT编译器编译后运行,并且引入了很好的页面缓冲机制。
(2) ASP.Net的配置模型引入了基于XML文件的“零安装”配置模型。零安装的含义是只需将配置文件Web.config,应用程序拷贝到系统指定的目录下即可,需要更改时直接在文件里更改并保存。
(3) 安全管理 ASP.Net提供了比传统ASP更强大可靠的安全管理。Asp.NET提供了三种验证方式以及两种类型的授权服务。
(4) 支持代码和页面内容的分离。回想以前编写Asp程序的时候的问题:代码逻辑混乱,难于管理。
(5) 提供了更好的状态管理,包括会话状态的管理和视图状态的管理。
(6) 大量的ASP.NET服务器端控件和对用户控件的开发的支持。
.NET Petshop充分的使用了服务器控件技术和会话状态管理。展示层的交互界面均采用aspx页面,后端有分离的逻辑代码。.NET Petshop共有19个aspx页面和代码逻辑。在这里一一里列出,并说明设置参数和其作用。
ASP.NET Web页面 | EnableSessionState | EnableViewState | 备注说明 |
Cart.aspx | true | true | 购物清单 |
Category.aspx | True | false | 产品分类的列表 |
CheckOut.aspx | Readonly | false | 订单确认 |
CreateNewAccount.aspx | False | true | 创建新帐号 |
Default.aspx | False | false | 系统首页面 |
EditAccount.aspx | False | true | 账号编辑 |
Error.aspx | False | false | 错误处理 |
Help.aspx | False | false | 帮助 |
OrderAddressConfirm.aspx | readonly | false | 订单地址确认 |
OrderBilling.aspx | True | true | 订单信息 |
OrderProcess.aspx | readonly | false | 下订单 |
OrderShipping.aspx | True | true | 订单地址和姓名 |
Product.aspx | False | false | 产品列表 |
ProductDetails.aspx | false | false | 产品详细信息 |
Search.aspx | false | false | 搜索 |
SignIn.aspx | false | false | 帐号登陆 |
SignOut.aspx | true | false | 帐号退出 |
ValidateAccount.aspx | false | false | 帐号创建确认 |
VerifySignIn.aspx | false | false | 账号登陆确认 |
.NET Petshop使用了很多用户控件,这些控件位于web/Inc目录下面。表格2对用户控件作出说明:
User Controls | EnableViewState | 备注说明 |
ControlAddress | true | 详细地址列表 |
ControlBanner | false | 页面底部的banner |
ControlCart | true | 购物清单 |
ControlFavList | false | 个人Favorites列表 |
ControlHeader | false | 页面顶部的菜单和链接 |
ControlStaticAddress | false | 不能编辑的地址信息 |
下面列出一段代码,说明这些参数的设置和用户控件的引用:
/* 摘自Cart.aspx */
<%@ Register TagPrefix="PetShop" TagName="Cart" Src="Inc/ControlCart.ascx" %>
<%@ Register TagPrefix="PetShop" TagName="Header" Src="Inc/ControlHeader.ascx" %>
<%@ Page language="c#" Codebehind="CheckOut.aspx.cs" AutoEventWireup="false" Inherits="PetShop.Web.CheckOut" EnableSessionState="readonly" enableViewState="False"%>
开始的两个Registe指令用于引用ControlCart和ControlHeader的用户控件,Page指令定义 ASP.NET 页分析器和编译器使用的页特定的属性。
在控件的排放位置使用下面的代码既可以显示该控件了:
<PetShop:Cart id="ctlCart" runat="server" allowedit="false" />
在上面的代码中allowedit一项是向实例化的ctlCart传递参数。我个人认为用户控件是一个很好的东西,可以简化web页面的开发模型又可以达到代码复用的目的,而且其开发非常简单,与aspx的开发基本是一样的。图1说明了设计模式下和运行时的用户控件。
图1:设计模式和运行时的ctlCart用户控件
接下来的文章我们将就系统配置和安全管理作进一步的说明。
.NET Petshop详解(五):petshop输出缓存设置
ASP.NET的输出缓存
衡量高性能、可缩放的web应用程序最重要的一个指标就是缓存了。ASP.NET提供了高性能的web应用程序的缓存功能,ASP.NET 有三种可由 Web 应用程序使用的缓存:
· 输出缓存,它缓存请求所生成的动态响应。
· 片断缓存,它缓存请求所生成的响应的各部分。
· 数据缓存,它以编程方式缓存任意对象。为支持这种缓存,ASP.NET 提供了全功能的缓存引擎,使程序员能够轻松地在请求间保留数据。
页的输出缓存是非常有用的。在海量的访问站点中,有些页面的访问频率占了非常大的比重,即使对这些页使用输出缓存很少的时间,也会减轻系统不少的负担,因为后面对这些页面的请求将不在执行创建该页的代码。
但是,这样显得不够灵活,页的请求可能的确是很多,然而在页面上我们缓存了所有的东西,无论是构造成本高还是构造成本低的部分。能否有一种可以缓存页的部分的数据呢?幸运的是ASP.NET提供了针对每个请求来创建或自定义该页的各部分。比如说我们可以对页面上构造成本很高的用户控件做片断缓存。
ASP.NET 缓存支持文件和缓存键依赖项,使开发人员可以使缓存项依赖于外部文件或其他缓存项。此项技术可用于在项的基础数据源发生更改时使该项无效。 ASP.NET可以将这些项存储在 Web 服务器上或请求流中的其他软件上,例如代理服务器或浏览器。这可以使您避免重新创建满足先前请求的信息,特别是当在服务器上创建时要求大量处理器时间或其他资源的信息。
Petshop的页缓存设置
我们可以可通过使用低级别的 OutputCache API 或高级别的 @ OutputCache 指令来实现页的输出缓存。 启用输出缓存后,当发出对页的第一个 GET 请求时创建一个输出缓存项。随后的 GET 或 HEAD 请求由该输出缓存项服务,直到该缓存请求过期。 输出缓存还支持缓存的 GET 或 POST 名称/值对的变体。
输出缓存遵循页的过期和有效性策略。如果某页位于输出缓存中,并且有一个过期策略标记指示该页自缓存起 60 分钟后过期,则在 60 分钟后将该页从输出缓存中移除。如果此后接收到另一个请求,则执行页代码,并且可以再次缓存该页。
下面的指令在响应时激活输出缓存:
<%@ OutputCache Duration="60" VaryByParam="none"%>
Duration和VaryByParam是必选参数,前者标识过期时间,后者表示GET 或 POST 名称/值对的字符串。如果不使用该属性,可是设置为none。在这里我们还要说明一个参数VaryByCustom,使用这个参数,我们可以自定义输出缓存要求的任意文本。除了在OutputCache指令里面申明该属性之外,我们还得在应用程序的 global.asax 文件的代码声明块中,重写 GetVaryByCustomString 方法来为自定义字符串指定输出缓存的行为。
举一列来说:
<%@ OutputCache VaryByParam="none" VaryByCustom="CategoryPageKey" Location="server" Duration="43200" %>
这里的VaryByCustom定义的为CategoryPageKey,那么在global.asax里面我们必须定义CategoryPageKey这个字符创输出缓存的行为,见下面代码。
public override string GetVaryByCustomString(HttpContext context, String arg) {
string cacheKey = "";
switch(arg) {
case "CategoryPageKey":
if (Request.IsAuthenticated == true) {
cacheKey = "QQQ" + context.Request.QueryString["category_id"] + context.Request.QueryString["requestedPage"];
}
else {
cacheKey = "AAA" + context.Request.QueryString["category_id"] + context.Request.QueryString["requestedPage"];
}
break;
case "SearchPageKey" :
if (Request.IsAuthenticated == true) {
cacheKey = "QQQ" + context.Request.QueryString["search_text"] + context.Request.QueryString["requestedPage"];
}
else {
cacheKey = "AAA" + context.Request.QueryString["search_text"] + context.Request.QueryString["requestedPage"];
}
break;
case "ProductPageKey" :
if (Request.IsAuthenticated == true) {
cacheKey = "QQQ" + context.Request.QueryString["name"] + context.Request.QueryString["product_id"] + context.Request.QueryString["requestedPage"];
}
else {
cacheKey = "AAA" + context.Request.QueryString["name"] + context.Request.QueryString["product_id"] + context.Request.QueryString["requestedPage"];
}
break;
case "ProductDetailsPageKey" :
if (Request.IsAuthenticated == true) {
cacheKey = "QQQ" + context.Request.QueryString["item_id"] + context.Request.QueryString["requestedPage"];
}
else {
cacheKey = "AAA" + context.Request.QueryString["item_id"] + context.Request.QueryString["requestedPage"];
}
break;
case "UserID" :
if (Request.IsAuthenticated == true) {
cacheKey = "UserID_In";
}
else {
cacheKey = "UserID_Out";
}
break;
}
return cacheKey;
}
从上面对CategoryPageKey字符创所作的行为来看,当我们的请求页面中含有对特定的category_id的某一分页显示的数据页的请求时,将调用缓存(自然是已经缓存了该页)。
下表列出了petshop的web应用程序的输出缓存设置。
ASP.NET WebForms | Cache setting | Duration |
ControlHeader | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="UserID" %> | 12 hours |
Default | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="UserID" %> | 12 hours |
Help | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="UserID" %> | 12 hours |
Category | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="CategoryPageKey " %> | 12 hours |
Product | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="ProductPageKey " %> | 12 hours |
ProductDetails | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="ProductDetailsPageKey " %> | 12 hours |
Search | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="SearchPageKey " %> | 12 hours |
显然petshop的web页面上部的ControlHeader是随着用户登陆的状态有关的,故其设置了VaryByCustom属性以来标识用户不同登陆状态的缓存版本。而Category页面由于可能被大量的访问,并且数据量很大,是十分有必要缓存的,但是由于数据的随机性很大,存在不同的版本,比如说是不同类别的Category,甚至不同的分页显示的数据页,在这里采用了VaryByCustom属性以缓存不同版本的页。
Petshop片断缓存
在前面我们提到ASP.NET可以提供页的局部数据的缓存,通常是一些构造代价较大的部分,诸如用户控件。在petshop里面,大量使用了用户控件(尤其是.NET示例程序Duwamish7.0使用了更多的用户控件,那些页面简直就是控件的拼装),用户控件的缓存设置方法和aspx页的缓存设置方法基本相同,在这里我们不再列出。只有ControlHeader控件使用了缓存设置,见上表。
.Net Petshop详解(二): petshop三层结构之DataTier
交互的三层综述
在前面的文章中,我们丛整个系统的角度了解了petshop的三层部署结构物理上的一个概貌,没有从应用程序的角度来说明是如何划分的,也不清楚数据展示层,中间层,数据层是如何交互的。为了更好的说明这个三层的结构以及每一层的具体细节问题,也让大家对整个应用有一个walkthrough,在这里我将说明从系统的角度来看(见参考图1),顾客在购物的过程中,系统的三层是如何的工作以及如何的交互。这个东西有点象UML里面的实例情景故事,对需求和分析的进一步是大有帮助的。
图1 .NET Petshop 架构
从这个情景中,我们可以很清晰的了解到Internet Customer在购物时系统的运作情形。首先通过数据展示层的用户交互界面Cart.aspx页面输入顾客购买的商品以及数量,然后这些购物清单由后台的应用程序(cart.aspx.cs)做相应的预处理(如安全验证,校验,数据的格式化等等),接着调用中间件(在程序结构里面就是一些.NET Assembly,封装了购物的商业逻辑),最后通过数据访问接口来更新数据库里面的数据(数据层)。
数据层(Database)
.NET Petshop的数据库并不是十分的庞大,总共有12个用户表和23个存储过程。Petshop数 据库里面存储的是用户数据,帐号数据,产品数据,用户配置数据,订单数据,库存数据以及供应商的数据。应用程序访问数据库的数据并不是直接的与数据库表打 交道,而是通过存储过程的运行来获取所需要的数据。这样的设计有一个好处就是,避免了频繁的表操作,而通过运行在服务器端的存储过程可以极大的提高运作效 率和提升访问数据的速度,同时也很好的屏蔽了数据库表的逻辑,使得数据库访问变成了数据库提供的服务访问。当然,也有人指责说这些存储过程迁移性是值得怀 疑的。
下面我将通过表格把这些数据库的基本表列出来(见表1),并一一做说明,希望对大家深入的理解有帮助。
表名称 | 备注信息 |
Account | 基本用户信息。 |
BannerData | 存储的是系统界面的banner图片的设置信息。 |
Category | 宠物的类别目录表(比如鱼类,狗类等等)。 |
Inventory | 宠物产品的存货信息。 |
Item | 单个产品的详细信息。 |
LineItem | 订单的每一项的详细信息,包括产品名称和数量,价格等。 |
Orders | 用户购物的订单,一个订单可以包括多项LineItem |
OrderStatus | 订单的状态 |
Product | 宠物的产品列表,一条Product可能包括多个Item |
Profile | 用户配置表,用于记录他们的favorites。 |
Signon | 用户帐号登陆表,因为常用,故从Account独立出来。 |
Supplier | 供应商信息。 |
通过上表我们知道了petshop数据库的数据库表的一些信息,但是这些表之间到底是一种什么样的关系呢?关心数据库建模和设计的人可能对这个问题比较感兴趣。在这里,我将给出图表(图2)说明这些数据库表之间的关系。图2中列出了这些表之间的关系和主键信息。
图2:petshop数据库物理设计模式
技巧:其实对于从事过数据库建模和设计的人都知道,得到上面的数据库模型图形并不是一件很困难的事情。正如我们前面提到的一样,我们可以用Visio做工程反转就可以得到上面这么美观的设计模型图了。同时,我们也可以在模型图中做修改设计,可以马上应用到你的物理数据库,使其保持同步。
说完了基本的数据库表,接下来我们看看存储过程。用微软的话说,只有设计称存储过程,才算是”cleaner separation of code from the middle-tier”,我个人觉得这样做是很好的。同样的,我把它列在一个表格里(表2)。
存储过程名称 | 备注信息 |
upAccountAdd | 增加一个帐号。 |
upAccountGetAddress | 获取用户的地址,主要用于下订单时注册地址与送货地址不一。 |
upAccountGetDetails | 获取帐号的详细信息。 |
upAccountLogin | 用户登陆验证。 |
upAccountUpdate | 更新用户帐号。 |
upCategoryGetList | 获取某个类别的产品列表。 |
upInventoryAdd | 增加指定的项到存活信息。 |
upInventoryGetList | 获取存货列表。 |
upItemAdd | 增加一项产品。 |
upItemGetDetails | 获取指定产品项的详细信息。 |
upItemGetList | 获取某一特定类别的产品的具体项目列表。 |
upItemGetList_ListByPage | 功能与上同,但是分页获取数据。 |
upOrdersAdd | 增加一项订单。 |
upOrdersGet | 获取某一订单的信息。 |
upOrdersGetDetails | 获取某一订单的详细信息。 |
upOrderStatusGet | 获取订单的状态。 |
upProductAdd | 增加一类别产品。 |
upProductGetList | 返回某类产品的列表。 |
upProductGetList_ListByPage | 与上同,但是分页获取结果数据。。 |
upProductSearch | 产品搜索。 |
upProductSearch_ListByPage | 与上同,但是翻页获取结果数据。 |
upProfileGetBannerOption | Banner的配置信息。 |
upProfileGetListOption | 获取用户配置信息。 |
表2:petshop存储过程列表
在这些存储过程里面使用了SQL Server2000的OpenXML的特性来代替传统的行集结果,使用在非常频繁的数据访问操作,可以减轻系统的负担。
好了,petshop的Datatier就说到这里了,在后面的文章我会就一个数据访问的实例再次解说存储过程的执行。接下来我将就中间层做介绍了。
.NET Petshop详解(三):petshop三层结构之MiddleTire
通过前面的文章,我们对.NET Petshop的整个结构有了一个大致的了解,也清楚的知道了数据库的设计模式和实现的细节,尤其值得一提的是通过存储过程访问数据库。在接下来的这篇文章里,我将和大家一起来探究一下.NET Petshop的中间层。
根据三层结构的设计原则,中间层封装的是业务逻辑和规则,在这个网络宠物商店的例子中,购物处理,订单处理,帐号管理,产品查询等等都是具体的业务逻辑,至于与用户交互并不是中间层要处理的问题。它处理是与具体的用户界面和交互无关,而仅仅是核心的商业规则和逻辑。.NET Petshop的中间层业务逻辑被封装为一个.NET 组件,它的命名空间为Pet Shop.Components(编译后在bin的文件夹里面有一个petshop.dll的文件)。图1是.NET Petshop解决方案中间层的类视图和文件视图。
图1:.NET Petshop解决方案中间层的类视图和文件视图
接下来,我们模拟顾客到百货超市采购日常用品的过程来说明运作的流程以及抽象出重要的概念(实际上User case,我们在领域分析的时候会这么做,并且是很重要的一步,从这里可以初步的发现在我们实施的系统中将要涉及到的逻辑实体,进而可以为数据库建模设计以及类设计提供参考)。
购物用例的业务分析:
1 客户有购买商品的意愿;
2 客户到登陆管理员处登记,且成功登记;
3 在登记处推一个购物车;
4 在超市内查找所购商品类别存放的货架;
5 在具体的货架上查找某一具体品牌的商品;
6 将符合意愿的商品放入自己的购物车;
7 重复4-6;
8 购物完毕;
9 到付款处计算总价格并付款;
10 打印购物清单;
11 退还购物车;
12 取走购物,购物完毕;
备注:在这个用例中,我们做了一些前提和假设,为的是方便.NET Petshop的分析,比如说在实际生活中根本就不需要第二步。
通过这个用例的分析,我们至少可以抽象出一下几个重要概念,并且能在应用程序里面找到对应的类:客户对应Customer、商品对应Product、购物车对应ShoppingCart、商品类别对应Category、具体商品对应Item、清单对应Order。
正如我前面说过的,这几个概念对于我们的业务建模和系统建模是非常有用的。正是通过这样的分析,在.NET Petshop的业务逻辑里面共有9个核心类和5个轻量级的数据结构类。同样的方式,我在这里列出这些类,并加以说明(见表1)。
类名称 | 说明 |
BasketItem | 代表购物车ShoppingCart里的一项购物商品。 |
Customer | 用于帐号管理和登陆验证。 |
CustomerDetails | 用户帐号的详细信息。 |
CustomerAddress | 用户帐号的地址信息。 |
Error | 用于登陆出错的帮助功能。 |
Item | 代表某类产品中的具体一项商品。 |
ItemResults | 搜索Item的结果集。 |
Order | 购物完毕后的购物清单和订单。 |
Product | 大类别里面的某类产品。 |
ProductResults | 搜索产品的结果集。 |
Profile | 用户的配置。 |
ShoppingCart | 购物车,用于购物的整个过程,直到下订单。 |
Database | 通过ADO.NET访问数据库,封装了具体的访问方法。 |
SearchResults | 模糊搜索的结果集。 |
表1:.NET Petshop中间层的类
CustomerAddress, CustomerDetails, ItemResults, ProductResults, and SearchResults这几个轻量级的数据结构类为在数据层和展示层之间提供了一种松散的数据绑定调用。这些类都被设计为有公开的属性,ASP.NET 的web页面可以通过这些属性访问数据。下面这段类的代码说明了这5个类是如何暴露自己的公开属性供展示层使用的。
public class ProductResults
{
private string m_productid;
private string m_name;
public string productid {
get { return m_productid; }
set { m_productid = value; }
}
public string name {
get { return m_name; }
set { m_name = value; }
}
}
在.NET Petshop详解(二)中我们就说过数据库的访问是通过存储来进行的,我们看看下面这部分代码就知道了:
public string Login(string userName, string password) {
string customerID;
// params to stored proc
Database data = new Database();
SqlParameter[] prams = {
data.MakeInParam("@username", SqlDbType.VarChar, 25, password),
data.MakeInParam("@password", SqlDbType.VarChar, 25, userName),
data.MakeOutParam("@CustomerID", SqlDbType.VarChar, 25)
};
// create data object and params
data.RunProc("upAccountLogin", prams); // run the stored procedure
customerID = (string) prams[2].Value; // get the output param value
// if the customer id is an empty string, then the login failed
if (customerID == string.Empty)
return null;
else
return customerID;
}
这段代码是Customer类的Login方法,它是通过将用户输入的用户名userName和密码password做为输入参数传递给存储过程upAccountLogin的,这个存储过程完成在Sigon用户帐号表里面查找该用户是否合法,最后返回一个字符串的用户ID值。在这里没有使用SQL查询语句,很好的分离了逻辑。具体的数据库访问是通过Database来完成的,我们将在后面的文章中继续探讨它的运作。
ShoppingCart是比较有意思的一个类,也是很重要的一个类。它是与状态有关的一个类,在.NET Petshop里面,它的状态是通过ASP.NET Session state来管理的,关于其进一步的细节留待后面讨论。
.NET Petshop的中间层的探讨先到此,我在这里只是抛砖引玉,很多的东西要深入代码才可以搞的更加清楚。欢迎大家继续与我一起关注下一篇.NET Petshop的数据展示层。
.NET Petshop详解(四):petshop三层结构之PresentationTier
在前面的文章中,我们已经就.NET Petshop的数据层和中间的业务逻辑层作了说明,接下来的文章中,我们将就数据展示层作探究。与前面的两层有着很大的差别的是,.NET Petshop的展示层用了很多Microsoft最新的web Forms技术即Asp.NET。因此,在解说展示层之前,我想就Asp.NET在.NET Petshop的开发中使用到的非常重要的特性做一些说明,以示区别:
(1) Asp.NET代码不再是解释型代码,可以经由JIT编译器编译后运行,并且引入了很好的页面缓冲机制。
(2) ASP.Net的配置模型引入了基于XML文件的“零安装”配置模型。零安装的含义是只需将配置文件Web.config,应用程序拷贝到系统指定的目录下即可,需要更改时直接在文件里更改并保存。
(3) 安全管理 ASP.Net提供了比传统ASP更强大可靠的安全管理。Asp.NET提供了三种验证方式以及两种类型的授权服务。
(4) 支持代码和页面内容的分离。回想以前编写Asp程序的时候的问题:代码逻辑混乱,难于管理。
(5) 提供了更好的状态管理,包括会话状态的管理和视图状态的管理。
(6) 大量的ASP.NET服务器端控件和对用户控件的开发的支持。
.NET Petshop充分的使用了服务器控件技术和会话状态管理。展示层的交互界面均采用aspx页面,后端有分离的逻辑代码。.NET Petshop共有19个aspx页面和代码逻辑。在这里一一里列出,并说明设置参数和其作用。
ASP.NET Web页面 | EnableSessionState | EnableViewState | 备注说明 |
Cart.aspx | true | true | 购物清单 |
Category.aspx | True | false | 产品分类的列表 |
CheckOut.aspx | Readonly | false | 订单确认 |
CreateNewAccount.aspx | False | true | 创建新帐号 |
Default.aspx | False | false | 系统首页面 |
EditAccount.aspx | False | true | 账号编辑 |
Error.aspx | False | false | 错误处理 |
Help.aspx | False | false | 帮助 |
OrderAddressConfirm.aspx | readonly | false | 订单地址确认 |
OrderBilling.aspx | True | true | 订单信息 |
OrderProcess.aspx | readonly | false | 下订单 |
OrderShipping.aspx | True | true | 订单地址和姓名 |
Product.aspx | False | false | 产品列表 |
ProductDetails.aspx | false | false | 产品详细信息 |
Search.aspx | false | false | 搜索 |
SignIn.aspx | false | false | 帐号登陆 |
SignOut.aspx | true | false | 帐号退出 |
ValidateAccount.aspx | false | false | 帐号创建确认 |
VerifySignIn.aspx | false | false | 账号登陆确认 |
.NET Petshop使用了很多用户控件,这些控件位于web/Inc目录下面。表格2对用户控件作出说明:
User Controls | EnableViewState | 备注说明 |
ControlAddress | true | 详细地址列表 |
ControlBanner | false | 页面底部的banner |
ControlCart | true | 购物清单 |
ControlFavList | false | 个人Favorites列表 |
ControlHeader | false | 页面顶部的菜单和链接 |
ControlStaticAddress | false | 不能编辑的地址信息 |
下面列出一段代码,说明这些参数的设置和用户控件的引用:
/* 摘自Cart.aspx */
<%@ Register TagPrefix="PetShop" TagName="Cart" Src="Inc/ControlCart.ascx" %>
<%@ Register TagPrefix="PetShop" TagName="Header" Src="Inc/ControlHeader.ascx" %>
<%@ Page language="c#" Codebehind="CheckOut.aspx.cs" AutoEventWireup="false" Inherits="PetShop.Web.CheckOut" EnableSessionState="readonly" enableViewState="False"%>
开始的两个Registe指令用于引用ControlCart和ControlHeader的用户控件,Page指令定义 ASP.NET 页分析器和编译器使用的页特定的属性。
在控件的排放位置使用下面的代码既可以显示该控件了:
<PetShop:Cart id="ctlCart" runat="server" allowedit="false" />
在上面的代码中allowedit一项是向实例化的ctlCart传递参数。我个人认为用户控件是一个很好的东西,可以简化web页面的开发模型又可以达到代码复用的目的,而且其开发非常简单,与aspx的开发基本是一样的。图1说明了设计模式下和运行时的用户控件。
图1:设计模式和运行时的ctlCart用户控件
接下来的文章我们将就系统配置和安全管理作进一步的说明。
.NET Petshop详解(五):petshop输出缓存设置
ASP.NET的输出缓存
衡量高性能、可缩放的web应用程序最重要的一个指标就是缓存了。ASP.NET提供了高性能的web应用程序的缓存功能,ASP.NET 有三种可由 Web 应用程序使用的缓存:
· 输出缓存,它缓存请求所生成的动态响应。
· 片断缓存,它缓存请求所生成的响应的各部分。
· 数据缓存,它以编程方式缓存任意对象。为支持这种缓存,ASP.NET 提供了全功能的缓存引擎,使程序员能够轻松地在请求间保留数据。
页的输出缓存是非常有用的。在海量的访问站点中,有些页面的访问频率占了非常大的比重,即使对这些页使用输出缓存很少的时间,也会减轻系统不少的负担,因为后面对这些页面的请求将不在执行创建该页的代码。
但是,这样显得不够灵活,页的请求可能的确是很多,然而在页面上我们缓存了所有的东西,无论是构造成本高还是构造成本低的部分。能否有一种可以缓存页的部分的数据呢?幸运的是ASP.NET提供了针对每个请求来创建或自定义该页的各部分。比如说我们可以对页面上构造成本很高的用户控件做片断缓存。
ASP.NET 缓存支持文件和缓存键依赖项,使开发人员可以使缓存项依赖于外部文件或其他缓存项。此项技术可用于在项的基础数据源发生更改时使该项无效。 ASP.NET可以将这些项存储在 Web 服务器上或请求流中的其他软件上,例如代理服务器或浏览器。这可以使您避免重新创建满足先前请求的信息,特别是当在服务器上创建时要求大量处理器时间或其他资源的信息。
Petshop的页缓存设置
我们可以可通过使用低级别的 OutputCache API 或高级别的 @ OutputCache 指令来实现页的输出缓存。 启用输出缓存后,当发出对页的第一个 GET 请求时创建一个输出缓存项。随后的 GET 或 HEAD 请求由该输出缓存项服务,直到该缓存请求过期。 输出缓存还支持缓存的 GET 或 POST 名称/值对的变体。
输出缓存遵循页的过期和有效性策略。如果某页位于输出缓存中,并且有一个过期策略标记指示该页自缓存起 60 分钟后过期,则在 60 分钟后将该页从输出缓存中移除。如果此后接收到另一个请求,则执行页代码,并且可以再次缓存该页。
下面的指令在响应时激活输出缓存:
<%@ OutputCache Duration="60" VaryByParam="none"%>
Duration和VaryByParam是必选参数,前者标识过期时间,后者表示GET 或 POST 名称/值对的字符串。如果不使用该属性,可是设置为none。在这里我们还要说明一个参数VaryByCustom,使用这个参数,我们可以自定义输出缓存要求的任意文本。除了在OutputCache指令里面申明该属性之外,我们还得在应用程序的 global.asax 文件的代码声明块中,重写 GetVaryByCustomString 方法来为自定义字符串指定输出缓存的行为。
举一列来说:
<%@ OutputCache VaryByParam="none" VaryByCustom="CategoryPageKey" Location="server" Duration="43200" %>
这里的VaryByCustom定义的为CategoryPageKey,那么在global.asax里面我们必须定义CategoryPageKey这个字符创输出缓存的行为,见下面代码。
public override string GetVaryByCustomString(HttpContext context, String arg) {
string cacheKey = "";
switch(arg) {
case "CategoryPageKey":
if (Request.IsAuthenticated == true) {
cacheKey = "QQQ" + context.Request.QueryString["category_id"] + context.Request.QueryString["requestedPage"];
}
else {
cacheKey = "AAA" + context.Request.QueryString["category_id"] + context.Request.QueryString["requestedPage"];
}
break;
case "SearchPageKey" :
if (Request.IsAuthenticated == true) {
cacheKey = "QQQ" + context.Request.QueryString["search_text"] + context.Request.QueryString["requestedPage"];
}
else {
cacheKey = "AAA" + context.Request.QueryString["search_text"] + context.Request.QueryString["requestedPage"];
}
break;
case "ProductPageKey" :
if (Request.IsAuthenticated == true) {
cacheKey = "QQQ" + context.Request.QueryString["name"] + context.Request.QueryString["product_id"] + context.Request.QueryString["requestedPage"];
}
else {
cacheKey = "AAA" + context.Request.QueryString["name"] + context.Request.QueryString["product_id"] + context.Request.QueryString["requestedPage"];
}
break;
case "ProductDetailsPageKey" :
if (Request.IsAuthenticated == true) {
cacheKey = "QQQ" + context.Request.QueryString["item_id"] + context.Request.QueryString["requestedPage"];
}
else {
cacheKey = "AAA" + context.Request.QueryString["item_id"] + context.Request.QueryString["requestedPage"];
}
break;
case "UserID" :
if (Request.IsAuthenticated == true) {
cacheKey = "UserID_In";
}
else {
cacheKey = "UserID_Out";
}
break;
}
return cacheKey;
}
从上面对CategoryPageKey字符创所作的行为来看,当我们的请求页面中含有对特定的category_id的某一分页显示的数据页的请求时,将调用缓存(自然是已经缓存了该页)。
下表列出了petshop的web应用程序的输出缓存设置。
ASP.NET WebForms | Cache setting | Duration |
ControlHeader | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="UserID" %> | 12 hours |
Default | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="UserID" %> | 12 hours |
Help | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="UserID" %> | 12 hours |
Category | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="CategoryPageKey " %> | 12 hours |
Product | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="ProductPageKey " %> | 12 hours |
ProductDetails | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="ProductDetailsPageKey " %> | 12 hours |
Search | <%@ OutputCache Duration="43200" VaryByParam="none" VaryByCustom="SearchPageKey " %> | 12 hours |
显然petshop的web页面上部的ControlHeader是随着用户登陆的状态有关的,故其设置了VaryByCustom属性以来标识用户不同登陆状态的缓存版本。而Category页面由于可能被大量的访问,并且数据量很大,是十分有必要缓存的,但是由于数据的随机性很大,存在不同的版本,比如说是不同类别的Category,甚至不同的分页显示的数据页,在这里采用了VaryByCustom属性以缓存不同版本的页。
Petshop片断缓存
在前面我们提到ASP.NET可以提供页的局部数据的缓存,通常是一些构造代价较大的部分,诸如用户控件。在petshop里面,大量使用了用户控件(尤其是.NET示例程序Duwamish7.0使用了更多的用户控件,那些页面简直就是控件的拼装),用户控件的缓存设置方法和aspx页的缓存设置方法基本相同,在这里我们不再列出。只有ControlHeader控件使用了缓存设置,见上表。