4.2 使用Table Service
Table Service相对来说是三个Storage Service中最好理解和最易于接受的,它主要用来存储结构化数据。但是Table Service却并不是一个关系型数据库。Table Service由两个部分组成:Table和Entity。当用户创建了一个Storage Service的时候就同时创建了Table Service。而在一个Table Service内用户可以创建多个Table,有些类似于数据库中的表。而在一个Table下面可创建多个Entity,类似于数据库中的记录。每一个Entity都可以创建多个Property,类似于数据库中的字段。它们之间的关系如图4-6所示。
图4-6 Table Service的结构关系
用户可以创建一个新的Table,然后添加、修改或删除Entity。同样,用户也可以删除Table。这些操作非常类似于使用普通的数据库。但是Table Service和普通的数据库是有很大区别的。
4.2.1 Table Service的特点
Table Service主要是存储结构化数据的,所有的数据都是一个个Entity,多个Entity在一起作为一个Table。而每一个Entity中最多包含255个Property。每个Property以键值对(Key-Value Pair)的方式保存,Key是字符串类型而Value可以是任意的.NET标准类型,比如字符串、整型、日期等。但是,Storage Service要求每一个Entity必须包含下面三个Property:Partition Key、Row Key和Timestamp。
· Partition Key:字符串类型,表示当前Entity的分区信息。这个Property对于Table Service自动纵向和横向扩展至关重要。
· Row Key:字符串类型,在给定Partition Key的分区下,Row Key要能唯一确定一个Entity。也就是说,Partition Key和Row Key可以作为Entity的联合主键。同时Entity也是基于Partition Key和Row Key值的顺序保存的,类似于SQL Server里面的聚集索引的概念。
· Timestamp:DateTime类型,作为版本控制信息表明当前Entity最后被操作的时间。使用者无法修改,由Windows Azure平台维护。
从表面上看,Table Service和关系型数据库很类似,但是它并不是一个关系型的数据存储服务。这主要表现在如下几点:
· 同一个Table下面的Entity可以拥有不同的Property。也就是说Entity里面包含什么Property不是在Table中确定的,而是每个Entity自己确定的。这一点和关系型数据库中数据表的概念是不一样的。
· 同一个Table下面的Entity,即使拥有同名的Property,它们的类型也可以是不同的。
· 无法像关系型数据库那样为一个Table建立多个索引。
· Table Service不支持外键、约束等关系数据库的功能。
· Table Service仅支持非常有限的事务操作。
虽然Table Service没有上述关系型数据库的特性,但是它也拥有自己特殊的功能,而这些功能都是目前关系型数据库所不具备的。
4.2.1.1 Table Service的动态扩展
Windows Azure平台为Table Service提供了自动的纵向和横向扩展功能。之前讲到,一个Table Service Entity必须包含三个Property,即Partition Key、Row Key以及Timestamp。其中,Partition Key就是用来进行自动纵向和横向扩展的。以用户信息为例,在Member Entity中以用户所在国家作为Partition Key,以用户注册的E-mail地址作为Row Key。应用程序刚刚开始运行的阶段,由于注册用户量不是很大,Windows Azure平台会将所有的用户信息都保存在一个存储节点中,也就是说所有的用户信息都保存在一起,如图4-7所示。
图4-7 所有Partition Key都在一个存储节点中
而随着用户的不断增加,这个Table的数据量开始大幅度上升。Storage Service不断侦测每一个Account下面每个Table的容量以及访问量,同时还会侦测每个Entity下面每种Partition Key所包含的数据量和访问量。如果发现某个Partition Key的数据量或访问量非常高,已经影响到了所在存储节点的性能,Windows Azure将会自动进行横向扩展,也就是说针对访问量或数据量很高的Partition Key,将其所有的Entity迁移到一个负载较小的存储节点上面。并且,这些操作都无须使用者控制,Windows Azure全部在后台自动完成。而且基于存储节点的负载均衡和路由,无论是应用程序还是RESTful API,都使用相同的URL和命令访问已经被迁移的数据。这就意味着应用程序代码不需要做任何调整。
而随着应用在全世界都很受欢迎,很多国家的注册用户已经非常多了,这时候Windows Azure就会依照不同Partition Key的负载分别进行横向扩展。例如,中国的用户最多将会被分到一个存储节点,英国和美国的用户被分配到另一个存储节点,而剩下的用户可能会被分配到第三个存储节点,如图4-8所示。
图4-8 Table Service根据Partition Key进行横向扩展
在上述横向扩展的同时,Table Service还支持纵向扩展,即将热点数据迁移到运行能力更强的存储节点中去。而且,纵向扩展和横向扩展一样也是完全自动且对使用者透明的。比如当前来自中国的用户非常多,那么Windows Azure则会将Partition Key为China的Entity迁移到处理能力更大的节点中以保证访问速度,如图4-9所示。
图4-9 Table Service的纵向扩展
由于Table Storage的动态纵向及横向扩展功能都是基于Partition Key来完成的,因此在选择Partition Key的时候要非常注意。如果应用程序使用完全一样的字符串作为Partition Key,那么Windows Azure平台就会将所有数据作为一个扩展单元处理而无法对其进行横向扩展,这样无疑对性能的提升是很有限的。但是如果应用程序对每一个Entity都使用不同的Partition Key(例如使用GUID作为Partition Key),由于每一个Partition Key的使用频度都不会很大,Windows Azure平台很可能就不会对其进行任何扩展。
一般来说,开发人员可以考虑将如下类型的数据作为Partition Key:
· 自然属性:比如国家名、人名的姓、日期(年份)、部门名称等。
· 基于算法:基于Row Key的哈希值、基于自增数的取模算法等。
无论何种方式,Partition Key的选取需要针对具体的业务逻辑和未来应用程序的发展趋势来决定,而没有一个绝对的最佳实践。比如当前的业务需求是做一个面向全球的在线照片分享网站,那么用户信息的Partition Key就可以使用国家名称或国家ID,而相册信息的Partition Key则可以使用用户的注册邮箱或用户ID。
4.2.1.2 事务支持
Table Service包含有限的事务支持。和关系型数据库不一样,Table Service只允许在Table和Partition Key相同的情况下对Entity进行事务操作。因此,如果某些业务逻辑需要事务支持,那么参与这些操作的Entity必须拥有相同的Partition Key,但是参与事务的Entity的Property可以不相同。例如在修改用户信息的时候,业务逻辑需要记录修改操作日志,一般来说会创建一个Member Table和一个Member Log Table,分别存储用户信息和修改日志信息。如果需要这两个修改操作必须在一个事务内完成,则应保证参与操作的Partition Key必须相同。
4.2.1.3 查询
Windows Azure Storage Service均支持通过REST API进行访问,包括对数据的增加、删除、修改和查询。对于Table Service而言,Windows Azure平台基于OData协议来实现数据服务。同时,Windows Azure SDK为开发人员提供了基于LINQ的方式访问Table Service REST API的功能。
参考
关于完整的Table Service REST API,请参考http://msdn.microsoft.com/en-us/library/dd179405.aspx。关于OData协议,请参考http://www.odata. org/。
在之前的介绍中提到过,Partition Key在动态扩展中有着至关重要的作用。同样的,Partition Key在查询的时候也非常重要。由于Table Service的自动横向扩展,不同Partition Key的Entity有可能保存在不同的存储节点中,那么如果查询操作需要跨存储节点执行,性能将会大打折扣。所以说开发人员在进行Table Service查询的时候,应尽可能地将Partition Key作为查询条件的一部分,以保证Windows Azure尽可能地在单一存储节点中完成操作。
另外,为了防止客户端的一次查询所返回的结果过多而造成网络堵塞,Table Service引入了Continuation Token机制。所谓Continuation Token就是指在查询结果过多的情况下,Table Service只会返回结果集的一部分,同时返回一个Continuation Token来表示还有剩余结果没有返回。客户端可以使用此Continuation Token对Table Service再次发起请求来获得后续的结果,直到没有Continuation Token返回为止。而具体来说,Table Service会在下列几种情况下返回Continuation Token:
· 如果查询操作出现了跨存储节点访问的情况。比如没有指定Partition Key的查询,Table Service会轮流查询所需要的所有存储节点,而在每个存储节点查询之后返回Continuation Token。
· 单次查询结果超过1000个记录的时候。
· 单次查询所耗费的时间超过5分钟则会把目前为止得到的结果返回,并返回Continuation Token。
接下来本书将通过示例来演示如何访问Table Service并且基于它的功能创建一个用来共享照片的小网站——Aurora。
4.2.2 使用Table Storage保存相册信息
为了便于演示,目前这个照片共享网站只支持一个人分享,算作是一种个人网站。照片以相册为单位。因此相册、照片的信息会保存到Table Storage中,而照片本身将会保存在BLOB Service中。首先看一下相册信息,它主要包括下面几个属性:
· Partition Key:默认属性之一,这里用使用者的E-mail地址作为Partition Key。在只有一个使者的情况下,所有的Partition Key都会是相同的。这并不符合此前推荐的Partition Key选择方案,但是考虑到未来这个网站会支持多人分享,所以现阶段将用户的E-mail地址作为Partition Key还是可以接受的。
· Row Key:默认属性之一,使用GUID作为Row Key以保证其唯一性。
· Timestamp:默认属性之一,由Windows Azure平台负责管理,用户无须设置。
· Name:相册的名字,字符串类型。
· Created On:相册创建的日期,日期类型。
· Viewed Count:这个相册被访问的次数,也就是相册中所有照片被访问次数的总和。
接下来打开Visual Studio,创建这个网站的Windows Azure项目和对应的Web Role。同时为了方便访问Windows Azure Storage Service,还会创建一个名为Aurora.Data的Class Library项目。这样,所有和Storage Service相关的操作都会被包含在这个项目中。并且在Aurora.Data项目中加入Windows Azure SDK提供的访问Storage Service的类库,位于C:\Program Files\Windows Azure SDK\v1.4\ref文件夹下面,包括:
· Microsoft.WindowsAzure.CloudDrive
· Microsoft.WindowsAzure.Diagnostics
· Microsoft.WindowsAzure.ServiceRuntime
· Microsoft.WindowsAzure.StorageClient
另外,由于Table Service的API是基于WCF Data Service创建的,因此还需要引用System.Data. Services.Client这个DLL文件。完成了上述操作之后,我们的Solution如图4-10所示。
图4-10 Aurora项目初始状态
参考
完成后的代码ch004_aurora_task001_begin.zip。
4.2.2.1 创建相册部分的Table Entity
在通过SDK类库访问Table Service时候,对于任何一个Entity需要事先在代码中添加一个对应的类。这个类里面的每个属性将会对应这个Table Entity的每个Property,而属性的类型就是这个Property的类型。在操作Table Service的代码中,会通过这个类将Entity的内容从Table Service里面读取出来;而在修改Entity的时候也需要先把修改内容赋值给相应的属性,然后更新到Table Service。由于之前介绍过,Windows Azure平台为Table Entity设定了三个必要的Property:Partition Key、Row Key和Timestamp,因此在代码中每个Entity对应的类也必须定义这三个属性。Windows Azure SDK已经包含了一个基类为我们预定义了这三个属性,即TableServiceEntity类。
下面开始创建一个相册的Entity类。在Aurora.Data项目下面创建一个名为Entities的文件夹,然后创建一个名为Album的类,它派生自TableServiceEntity这个基类(TableServiceEntity类定义在Microsoft.WindowsAzure.StorageClient这个命名空间),代码如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.WindowsAzure.StorageClient; namespace Aurora.Data.Entities { public class Album : TableServiceEntity { } }
TableServiceEntity基类中定义了两个Protected的构造函数:一个是无参数的,另外一个是需要传入Partition Key和Row Key两个参数的。由于基类TableServiceEntity中已经定义了三个Table Entity必需的属性,因此在派生类中无须再定义了。接下来就依照之前确定的相册Entity所包含的成员在Album类中添加相应的属性,完成后的代码如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.WindowsAzure.StorageClient; namespace Aurora.Data.Entities { public class Album : TableServiceEntity { public string Name { get; set; } public DateTime CreatedOn { get; set; } public long ViewedCount { get; set; } public Album(string memberEmail) : base(memberEmail, Guid.NewGuid().ToString()) { } public Album() : base() { } } }
由于相册Entity使用用户的E-mail地址作为Partition Key,使用GUID作为Row Key,所以在上面的代码中重写了构造函数,只需要传入用户E-mail地址就可以实例化一个Album类。同时还需要创建一个无参数的构造函数,为了稍后使用这个类作为ASP.NET MVC中的View Model。
4.2.2.2 使用Table Service Context访问Table Service
Windows Azure SDK通过TableServiceContext类执行对Table Service和Entity的增删改查操作。为此,在Aurora.Data项目中创建一个名为AlbumDataContext的类,并且派生自SDK中的TableServiceContext基类(TableServiceContext同样位于Microsoft.WindowsAzure.StorageClient命名空间),代码如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.WindowsAzure.StorageClient; namespace Aurora.Data { public class AlbumDataContext : TableServiceContext { } }
基类TableServiceContext有一个Public构造函数,需要传递两个参数,分别是:
· baseAddress:Table Service对外的URL地址,也就是在Developer Portal上选择了Storage Service节点的时候右侧属性栏中显示的Table Service URL。
· Credentials:Storage Service的访问凭据,即Storage Service的Account Name和Account Key的组合。在SDK中它被封装成了StorageCredentials类型。
在新创建的AlbumDataContext中重写这个构造函数,通过类型为CloudStorageAccount的参数account来调用其基类构造函数。而CloudStorageAccount这个类包含在Microsoft.WindowsAzure命名空间下,提供了更加方便地操作Storage Service Account的功能,是使用Storage Service必不可少的类,它主要包括如下成员:
· TableEndpoint:返回Table Service的URL地址。
· BlobEndpoint:返回BLOB Service的URL地址。
· QueueEndpoint:返回Queue Service的URL地址。
· Credentials:返回StorageCredentials对象。它包含了Storage Service的Account信息,即实例化TableServiceContext所需要的参数。
· SetConfigurationSettingPublisher:静态方法,设定如何获取Storage Service的Account信息。如果用户在Role Setting界面里面设定了访问Storage Service的连接字符串,那么可以通过这个方法设置从CSCFG文件里面获取Account信息。
· FromConfigurationSetting:静态方法,从CSCFG文件中指定的Key获取Storage Account信息。如果没有通过上面介绍的SetConfigurationSettingPublisher方法设定如何获取配置信息,那么调用此方法的时候将会引发异常。
· Parse和TryParse:静态方法,尝试直接通过连接字符串生成Account信息。
通过上述介绍,现在可以在AlbumDataContext的构造函数中通过传入CloudStorageAccount类型的参数来调用基类构造函数。并且,为了方便以后的使用还可以将此参数缓存起来。修改后的代码如下所示。
namespace Aurora.Data { public class AlbumDataContext : TableServiceContext { private CloudStorageAccount _account; public AlbumDataContext(CloudStorageAccount account) : base(account.TableEndpoint.AbsoluteUri, account.Credentials) { _account = account; } } }
通过代码首次使用Album这个Table的时候,它还并不存在于Table Service中,为此需要首先创建这个Table,然后才能对其进行Entity的增删改查操作。创建Table的操作可以通过CloudTableClient这个类来完成。它负责操作Table Service的Table本身,主要包括如下成员:
· CreateTable:创建一个Table。
· CreateTableIfNotExist:如果Table不存在则创建这个Table,否则不做任何操作。
· CreateTablesFromModel:静态方法,通过Table Service Entity的类来创建一个新的Table。
· DeleteTable:删除一个Table。
· DeleteTableIfExist:如果Table存在则删除这个Table,否则不做任何操作。
· DoesTableExist:返回指定的Table是否存在。
· ListTables:列出当前Account下的所有Table名称。其中一个重载函数可以通过参数指定Table的前缀,这样返回符合这个前缀的所有Table的名称。
不仅如此,CloudTableClient类对于上述操作都提供了异步调用模型,也就是说可以通过类似于BeginCreateTable和EndCreateTable这一对方法来异步调用Table Service。
CloudTableClient类也是通过传入Table Service的URL和Credentials来实例化,所以可以直接在AlbumDataContext的构造函数中初始化这个类,然后调用CreateTableIfNotExist方法创建对应的Table。修改后的代码如下所示。
public AlbumDataContext(CloudStorageAccount account) : base(account.TableEndpoint.AbsoluteUri, account.Credentials) { _account = account; var client = new CloudTableClient(_account.TableEndpoint.AbsoluteUri, _account.Credentials); client.CreateTableIfNotExist("Album"); }
随后便可以为AlbumDataContext类添加增删改查的功能。这些功能在它的基类TableServiceContext中都有实现,只不过它们都是基于弱类型的,也就是说必须传入Table的名字和类型为object的Entity对象。这些操作主要包括:
· AddObject:通过传入Entity的名字和实例添加一个新的Entity。
· DeleteObject:通过传入Entity实例来删除这个Entity。
· UpdateObject:通过传入Entity实例来更新这个Entity,传入的Entity是更新后的Entity。
· CreateQuery<T>:通过传入的Entity名字以及泛型参数T返回Table Service的查询结果集DataServiceQuery<T>。由于它实现了IQueryable<T>接口,因此开发人员可以通过LINQ方式对Table Service进行条件查询等操作。
· SaveChanges:提交所做的修改。同时此方法还提供了几个重载函数,设置不同的提交原则,它们包括:
■ None:对每一个操作都使用不同的请求,但是如果其中有一个操作失败了,则停止其后的所有请求。无参数的SaveChanges函数采用的就是这个提交原则。
■ Batch:所有操作都基于同一个请求,如果有任何一个请求失败就会回滚所有的操作。这种方式即使用了Table Service的事务功能Group Transaction。关于Group Transaction功能请参见本书4.2.4节。
■ ContinueOnError:与None类似,每一个操作都使用不同的请求,但即使其中的某个操作失败,后续的操作还会继续执行。
■ ReplaceOnUpdate:针对Entity的更新操作,不管修改前后Entity的属性值是否相同,所有的属性都会被更新。
由于现在已经确认了当前访问类操作的Entity的类型就是Album,Table的名字也是Album,因此可以将上述的操作简化。修改后的代码如下所示。
public class AlbumDataContext : TableServiceContext { ... ... public IQueryable<Album> Albums { get { return CreateQuery<Album>(tableName); } } public void AddAlbum(Album album) { AddObject(_tableName, album); SaveChanges(); } public void UpdateAlbum(Album album) { UpdateObject(album); SaveChanges(); } public void DeleteAlbum(Album album) { DeleteObject(album); SaveChanges(); } }
在这个数据访问类中,Entity修改后直接调用了SaveChanges方法立即将操作提交。到目前为止我们已经准备好了数据访问层的所有代码,通过Album类和AlbumDataContext类就可以实现对Album这个Table Service Entity的增删改查操作。接下来在ASP.NET MVC部分通过调用AlbumDataContext来实现相册的创建、浏览、修改和删除功能。
4.2.2.3 使用Album Data Context实现相册管理功能
首先实现创建相册的页面和相关功能。在此之前,需要先将应用程序使用的Storage Service Account信息设置好,这样才能将此信息传递给AlbumDataContext类。这个操作可以在Azure项目的Setting界面里完成,在此前3.3.1节的Settings标签页部分已经介绍过了,这里不妨再复习一下,如图4-11所示:
图4-11 在项目中设定访问Storage Service的配置信息
01 在Solution Explorer中选择的Windows Azure项目,选中Roles目录下面需要设置的Role——Aurora.Website,双击。
02 打开的设置页面里从左侧选择Settings标签。
03 击上方的Add Setting,然后在新建的Setting中输入名字DataConnectionString,Type部分选择Connection String,Value部分单击右侧的按钮弹出对应的连接字符串设定对话框。
04 于目前项目还处在本地开发状态,因此在对话框中选择第一个选项Use the Windows Azure storage emulator,使用本地模拟器的Storage Service。
05 击OK按钮,保存并关闭这个Role的Setting界面。
经过上述操作,访问Storage Service的连接字符串就被保存到CSCFG文件中了。而如果需要使用TableServiceContext这个类,就必须事先从CSCFG文件中读取出Storage Service的Account信息,也就是说需要通过CloudStorageAccount的SetConfigurationSettingPublisher方法指定如何取得这个信息。所以首先需要到ASP.NET MVC项目中指定这个连接字符串的获取方法。
打开Aurora.Website项目并双击Global.asax文件,在网站启动的时候设定连接字符串的读取方法,加入如下代码。
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes); CloudStorageAccount.SetConfigurationSettingPublisher((configName, configGetter) => { configGetter.Invoke(RoleEnvironment.GetConfigurationSettingValue (configName)); }); }
这样,就可以在需要获取Storage Service Account的时候调用如下代码。
CloudStorageAccount.FromConfigurationSetting("DataConnectionString")
注意
在Windows Azure SDK 1.3版本发布之前,调用SetConfigurationSetting Publisher代码可以放在WebRole.cs中的OnStart方法里面执行。但是在1.3版发布之后,由于Windows Azure平台对于Web Role的部署方式有所变化,导致上述代码必须放到网站的Application_Start方法中才可以。更详细的信息,请参见本书8.4节。
1.创建相册——添加Table Entity
完成了上述准备工作,接下来开始实现相册的具体功能。在Aurora.Website项目中创建一个新的AlbumController。然后创建一个Create方法来显示创建相册的页面,并且直接将Album Entity作为参数传递给View。由于目前网站只支持一个用户,所以这里可以直接将这个用户的E-mail地址作为参数传递给Album Entity。修改后的代码如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using Aurora.Data.Entities; namespace Aurora.Website.Controllers { public class AlbumController : Controller { [HttpGet] public ActionResult Create() { return View(new Album("admin@aurora.com")); } } }
之后创建与之相对应的View。由于我们创建相册时需要指定相册的名称,所以View中也只需要让用户输入相册名称就足够了。视图的代码如下所示。
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits= "System.Web.Mvc.ViewPage<Aurora.Data.Entities.Album>" %> <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> Aurora - Create Album </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2>Create Album</h2> <% using (Html.BeginForm()) {%> <%: Html.ValidationSummary(true) %> <fieldset> <legend>Fields</legend> <div class="editor-label"> <%: Html.LabelFor(model => model.Name) %> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.Name) %> <%: Html.ValidationMessageFor(model => model.Name) %> </div> <p> <input type="submit" value="Create" /> </p> </fieldset> <% } %> <div> <%: Html.ActionLink("Back to List", "Index") %> </div> </asp:Content>
由于此前的代码在Create方法中实例化了Album类并通过参数传递进来,因此此时Album中的Partition Key和Row Key都已经设定好了,所以在视图中还需要通过隐藏域把它们传递回Controlloer中,对应的View代码如下所示。
<div class="editor-label"> <%: Html.LabelFor(model => model.Name) %> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.Name) %> <%: Html.ValidationMessageFor(model => model.Name) %> </div> <%: Html.HiddenFor(model => model.PartitionKey) %> <%: Html.HiddenFor(model => model.RowKey) %> <p> <input type="submit" value="Create" /> </p>
然后在AlbumController中创建一个对应的POST的Create方法来实际执行创建相册的功能,也就是调用之前实现的AlbumDataContext类。在这个方法中将Album类作为参数传递进来,它包含了刚才在视图页面里输入的相册名称。
public class AlbumController : Controller { [HttpGet] public ActionResult Create() { return View(new Album("admin@aurora.com")); } [HttpPost] public ActionResult Create(Album album) { } }
接下来进行必要的检查工作,包括相册的名字不能为空,以及没有重名相册存在——而后者需要首先从Table Service中获取所有已经存在的相册。在代码中创建AlbumDataContext实例,将Storage Service Account信息通过参数传递进去。由于此前在Application_Start方法中已经设置了读取配置信息的操作,所以这里直接调用CloudStorageAccount的FromConfigurationSetting方法,就可以获得CSCFG文件中的连接字符串并得到Account信息了。之后通过定义在AlbumDataConext中的Albums属性获取到目前已存在的相册信息,代码如下所示。
[HttpPost] public ActionResult Create(Album album) { // validation if (string.IsNullOrWhiteSpace(album.Name)) ModelState.AddModelError("Name", "Please input the name of the album."); var ctx = new AlbumDataContext(CloudStorageAccount.FromConfigurationSetting("DataConnectionString")); if (ModelState.IsValid) if (ctx.Albums.Any(al => string.Compare(al.Name, album.Name, true) == 0)) ModelState.AddModelError("Name", "Album name is existed."); }
提示
实际上,由于Albums返回的对象实现了IQueryable<T>接口,所以只有实际执行了迭代器操作才会从Storage Service获取Entity的信息。而且由于TableServiceContext是基于WCF Data Service的,大部分的LINQ语句都会被转换成OData标准的查询语句执行。
如果检测通过,则可以为Album Entity中的其他属性设置初始值,包括创建时间、浏览次数等,然后调用AlbumDataContext中的AddAlbum方法向Table Service中创建这个相册的Entity。
[HttpPost] public ActionResult Create(Album album) { // validation ... ... // create new album if (ModelState.IsValid) { album.CreatedOn = DateTime.Now; album.ViewedCount = 0; ctx.AddAlbum(album); return RedirectToAction("Index", "Home"); } else { return Create(); } }
在本地模拟器环境中运行当前的项目,浏览器打开了默认的首页。直接在浏览器内输入地址http://127.0.0.1:81/Album/Create,然后在输入框内写入要创建的相册名称,比如The Forbidden City,然后按Create按钮提交。
如图4-12所示,应用程序在执行中出现了异常。从异常的提示来看,这个错误是因为在检测是否有重名相册操作时调用了一个不支持的方法Any。我们知道,用来访问Table Service的AlbumDataContext类,也就是其基类TableServiceContext是基于WCF Data Service实现的,也就是说它LINQ功能的实现也是基于WCF Data Service,所有的LINQ查询语句最终都会被翻译为对应的OData查询字符串并通过HTTP请求发送给Storage Service。但是,TableServiceContext并不是完全实现了所有WCF Data Service的LINQ操作,如果应用程序使用了那些不支持的LINQ操作就会引发这个异常。例如代码中使用的Any方法就是不支持的。
图4-12 使用LINQ的Any方法导致异常
图4-13展示了使用TableServiceContext访问Table Service时,代码是如何通过Windows Azure SDK的类库、WCF Data Service以及OData最终发送给Windows Azure平台的。
图4-13 通过代码访问Table Service的流程
表4-3列举了目前经常用到的一些LINQ扩展方法在Table Service的支持状况。更加详细的信息可参考http://msdn.microsoft.com/en-us/library/dd135725.aspx。
表4-3 Table Service支持的LINQ扩展
现在需要修改一下判断相册名称是否重复的代码,使用WCF Data Service允许的LINQ扩展,然后重新运行添加一个新的相册。这一次应用程序成功显示了主界面,表明信息已经加入到本地的Storage Service环境中了。如果再次加入一个同名的相册将会得到如图4-14所示的错误信息,表明已经有同名的相册存在了。
图4-14 添加同名相册失败
而目前还没有显示相册列表的功能,所以还没有办法在页面中看到已经创建好的相册。
参考
完成后的代码ch005_aurora_task001_end.zip。
2.相册列表——遍历Table下的Entity
刚才我们完成了创建相册的功能,但是由于没有显示相册列表的功能,所以目前为止还无法看到创建好的相册信息。下面我们就来完成这个功能。相册列表的功能主要是通过Table Service的查询实现的,其实在之前创建相册代码中已经使用过这个功能了——创建相册之前要进行相册名称是否重复的检测,而这个检测就是通过Table Service的查询功能实现的。
首先稍微修改一下ASP.NET MVC网站,将默认主页设置为相册列表页面,即需要修改一下Global.asax文件中ASP.NET MVC的路由配置。具体的操作这里就不再赘述,完成后的代码如下所示。
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Album", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); }
参考
修改后的代码请参考ch006_aurora_task002_begin.zip。
接下来在AlbumsController中创建Index方法,从Table Service中获取所有的相册信息。同样的,实例化AlbumsDataContext然后通过Albums方法读取到所有的Album Entity。由于Table Service本身不支持OrderBy和OrderByDescending扩展方法,所以需要首先通过ToList方法将所有的相册信息取出来,然后再调用OrderByDescending方法按照创建时间长短的倒序排列(在这种状况下OrderBy方法是基于LINQ to Object的方式执行的)。最后将获取到的相册列表作为View Model传递给View。
[HttpGet] public ActionResult Index() { var ctx = new AlbumDataContext(CloudStorageAccount.FromConfigurationSetting ("DataConnectionString")); var albums = ctx.Albums .ToList() .OrderByDescending(al => al.CreatedOn); return View(albums); }
然后创建对应的View。一般来说,Table Service的Partition Key、Row Key和Timestamp是不会显示给最终用户的,但是为了方便演示我们会将这三个字段都显示到页面上。完成的View页面主要部分如下所示。
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2>My Albums</h2> <table> ... ... <% foreach (var item in Model) { %> <tr> <td> <%: Html.ActionLink("Edit", "Edit", new { /* id=item. PrimaryKey */ }) %> | <%: Html.ActionLink("Details", "Details", new { /* id=item. PrimaryKey */ })%> | <%: Html.ActionLink("Delete", "Delete", new { /* id=item. PrimaryKey */ })%> </td> <td><%: item.Name %></td> <td><%: item.CreatedOn.ToString() %></td> <td><%: item.ViewedCount %></td> <td><i><%: item.PartitionKey %></i></td> <td><i><%: item.RowKey %></i></td> <td><i><%: item.Timestamp.ToString() %></i></td> </tr> <% } %> </table>
如图4-15所示,在本地模拟器中运行代码后便可以看到刚才创建的相册信息显示在了列表里面,同时还可以看到三个默认属性Partition Key、Row Key和Timestamp的值。通过页面顶端的Create Album菜单可以创建一个新的相册。
图4-15 相册列表页面
参考
完成后的代码请参考ch007_aurora_task002_end.zip。
3.完善相册的功能——修改和删除相册
最后,将相册管理的功能进行完善,首先是修改相册的功能。修改相册的操作和创建相册非常相似,唯一不同的是,应用程序需要首先获取到这个相册的信息,显示在修改界面上,然后将修改后的结果更新回Table Service。由于可以通过Partition Key和Row Key唯一确定一个Entity,所以这里需要将待修改相册的Partition Key和Row Key传递进来,然后读取详细的相册信息并显示在界面上。用户修改提交之后,将修改后的相册名字再通过AlbumDataContext的UpdateAlbum方法更新回Table Service就可以了。
首先在AlbumController中创建一个名为Edit的方法,然后通过传入的参数Partition Key和Row Key获取相册信息并传入到View Model中。
[HttpGet] public ActionResult Edit(string partitionKey, string rowKey) { var ctx = new AlbumDataContext(CloudStorageAccount.FromConfigurationSetting ("DataConnectionString")); var album = ctx.Albums .Where(al => al.PartitionKey == partitionKey && al.RowKey == rowKey) .FirstOrDefault(); if (album == null) { return RedirectToAction("Index"); // invalid album just back to the list } else { return View(album); } }
接下来实现相册列表中的修改链接,将每个相册的Partition Key和Row Key设置到ASP.NET MVC的Route Data中生成不同的链接地址。完成后的代码如下所示。
<% foreach (var item in Model) { %> <tr> <td> <%: Html.ActionLink("Edit", "Edit", new { partitionKey = item. PartitionKey, rowKey = item.RowKey }) %> | <%: Html.ActionLink("Details", "Details", new{ /*id=item.PrimaryKey */})%> | <%: Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })%> </td>
之后创建修改相册信息的View,其内容和创建相册用的View几乎一样,这里就不再重复介绍了。然后在对应的POST方法中检查修改的相册名字是否合法,如果合法则调用UpdateAlbum方法更新Table Service中的Entity。
合法性检查和创建相册非常相似,首先检查相册名字不能是空的,然后检查修改后的相册名字是不是已经存在了。如果名字合法,那么通过AlbumDataContext类以及这个相册的Partition Key和Row Key取出对应的Entity,将新的名字赋给Name属性,最后调用UpdateAlbum方法进行更新。完整的更新部分代码如下所示。
[HttpPost] public ActionResult Edit(Album album) { // validation if (string.IsNullOrWhiteSpace(album.Name)) ModelState.AddModelError("Name", "Please input the name of the album."); if (ModelState.IsValid) { var ctx = new AlbumDataContext(CloudStorageAccount.FromConfiguration Setting ("DataConnectionString")); var albumDuplicated = ctx.Albums.Where(al => al.Name == album.Name && (al.PartitionKey != album.PartitionKey || al.RowKey != album. RowKey)) .FirstOrDefault(); if (albumDuplicated != null) ModelState.AddModelError("Name", "Album name is existed."); // update the album if (ModelState.IsValid) { var albumToUpdate = ctx.Albums .Where(al => al.PartitionKey == album.PartitionKey && al.RowKey == album.RowKey) .FirstOrDefault(); albumToUpdate.Name = album.Name; ctx.UpdateAlbum(albumToUpdate); return RedirectToAction("Index"); } } return Edit(album.PartitionKey, album.RowKey); }
在上面的代码中,我们没有直接使用参数传递进来的Album对象作为UpdateAlbum方法的参数,而是从Table Service里面取出了这个Album Entity,然后将相册名字赋值给它,之后再进行UpdateAlbum操作。这是为什么呢?主要原因是由于当应用程序通过TableServiceContext执行添加、修改和删除Entity的操作时,只有执行了SaveChanges方法后修改的内容才会被提交到Table Service中,这就要求TableServiceContext类对所执行的操作提供Tracking的功能。如果直接使用参数传递进来的Album Entity进行修改操作,由于这个Entity并不是通过TableServiceContext获取的,因此没有必要的Tracking信息,就会在最终调用SaveChanges时引发异常。所以在对Entity进行修改和删除操作的时候,必须首先从Table Service中取得Entity对象然后再执行实际的修改和删除操作。
完成之后在本地模拟器运行,首先看到的是相册列表,选择一个相册单击前面的Edit链接,如图4-16所示。
图4-16 相册列表中的修改相册信息链接
进入相册信息修改页面,可以看到当前相册的名字已经显示在页面上了。如果将其修改为一个已经存在的相册名字并提交,那么检测程序会提示我们这个名字已经存在了,如图4-17所示。
图4-17 修改相册名为一个已存在的名字引发错误
将其修改为别的名字,可以看到Table Service更新了这个相册的信息并且返回相册列表。同时注意到,即使在代码中没有设置Timestamp字段,但是修改后的相册Entity的Timestamp属性值已经变为当前的时间了(UTC时间),如图4-18所示,说明Timestamp是由Table Service自己维护的。
图4-18 修改后的相册信息
对于删除相册的功能也是以类似的方法实现,首先在Controller中创建一个Delete方法,传入要删除相册的Partition Key和Row Key,给用户一个确认删除的页面提示。如果单击确认按钮,那么就可以调用AlbumDataContext中的DeleteAlbum方法将这个Entity删除掉并返回相册列表页面。这里我们就不再详细介绍了,有兴趣的读者可以自己动手试一下。
参考
完成后的代码,包括修改相册和删除相册的功能,请参考ch008_aurora_taks003_end.zip。
上面的代码已经完成了对于相册的所有管理功能,我们可以使用同样的方法实现对于照片的管理功能。当然目前来说只能在Table Service中创建、修改和删除照片信息,还不能上传照片。
不过准备实现照片部分功能的时候可以看到,除了要创建一个对应照片Entity的Photo类以外,还要创建对于照片的操作类,即一个逻辑上基本和AlbumDataContext类一致的PhotoDataContext类,它们之间的重复代码很多。那么有没有办法简化和重用这部分代码呢?接下来,我们就来看看如何将数据访问层设计中常用的Repository模式引入到Table Service的数据访问代码中。
4.2.3 基于Repository模式的Table Service数据访问层
在开始进行数据访问层代码重构之前,首先看一下如果按照已经创建好的AlbumDataContext方式来实现新的PhotoDataContext类,那么它将会是什么样子。
首先,PhotoDataContext也需要同样的增删改查操作,基本的逻辑和AlbumDataContext一样,不同之处在于:
· 对于获取Entity集合操作,也就是查询操作,只需要调用基类中不同泛型参数的CreateQuery方法,并且传入不同的Table名即可。
· 对于添加、修改和删除操作,会调用相同的基类方法AddObject、UpdateObject和DeleteObject,只不过传入不同的Entity对象。
通过上述分析可以看出,如果将Table Entity类的名字作为这个Table在Table Service中的名字,那么这些函数都可以通过泛型参数的方法统一。同时,由于对于不同的Entity其所访问的Table Service Account都是一样的,所以实例化函数不受影响。这样的话,完全可以将这些操作统一到一个泛型基类下面,然后通过指定不同的泛型参数来实现针对不同Entity的DataContext类。
4.2.3.1 Repository模式
Repository模式是广泛使用在数据访问层,特别是基于强类型的数据访问层中的一种设计模式。Repository模式通过强类型的数据实体,基于泛型接口和泛型基类实现了统一的数据添加、修改、删除和查询逻辑。视情况而定,可以基于这个泛型接口和泛型基类实现一个动态的数据访问类,也可以基于每种数据实体分别实现各自的数据访问类。图4-19展示了泛型的Repository接口和基类以及针对相册Entity实现的Repository类。
图4-19 针对每个具体的Entity实现的访问层
图4-20是基于泛型方法的Repository类,实现了针对任意Entity的通用Repository类。针对不同Entity的操作可以通过各个方法的泛型参数指定。
图4-20 基于泛型方法实现的通用Repository模式
Table Service的数据访问层基类TableServiceContext完全符合上述要求,因此可以将所有Entity的操作都基于统一的Repository基类实现,然后针对不同的Entity派生不同的Repository类,从而达到简化代码的目的。下面我们以第一种Repository模式创建新的数据访问层代码。
4.2.3.2 创建Repository接口和基类
由于Repository模式需要基于强类型数据实体,因此首先需要创建针对每个Entity的数据实体。Windows Azure SDK已经定义了一个数据实体基类,即刚才见到的TableServiceEntity。但是为了获得更好的扩展性,在这里我们定义一个中间基类用来封装应用程序对于Entity的一些通用操作,例如对于Partition Key和Row Key的指定和通用属性的定义等。
创建一个TableServiceEntityBase抽象类,它派生自TableSericeEntity,同时在构造函数中封装了之前对于Partition Key和Row Key的操作。并且根据业务逻辑的需要将CreatedOn属性定义在这个类里面,保证应用程序中所有的Entity都附带此属性,完成后的代码如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.WindowsAzure.StorageClient; namespace Aurora.Data { public abstract class TableServiceEntityBase : TableServiceEntity { public DateTime CreatedOn { get; set; } protected TableServiceEntityBase() : this(string.Empty, string.Empty) { } protected TableServiceEntityBase(string partitionKey) : this(partitionKey, Guid.NewGuid().ToString()) { } protected TableServiceEntityBase(string partitionKey, string rowKey) : base(partitionKey, rowKey) { } } }
接下来创建最基础的接口ITableServiceReporsitory,它通过泛型参数获得所要操作的Entity的类型,同时在这个接口里面定义好对Entity的添加、修改、删除和查询的方法,完成后的代码如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Aurora.Data { public interface ITableServiceRepository<T> where T : TableServiceEntityBase { IEnumerable<T> Retrieve(); void Create(T entity); void Update(T entity); void Delete(T entity); } }
最后创建TableServiceRepository基类。这个类实现了刚才定义的ITableServiceReporsitory接口,然后参考之前创建好的AlbumDataContext类,通过基于泛型参数的Entity类型来实现抽象Table Service Entity的增删改查功能。另外,在Create方法中,我们为Entity的CreatedOn属性设定了当前的日期和时间,这样在使用的时候就不需要关心这个字段的赋值问题了。实现后的代码如下所示。
namespace Aurora.Data { public abstract class TableServiceRepository<T> : TableServiceContext, ITableServiceRepository<T> where T : TableServiceEntityBase { private CloudStorageAccount _account; private string _tableName; public TableServiceRepository(CloudStorageAccount account) : base(account.TableEndpoint.AbsoluteUri, account.Credentials) { _account = account; _tableName = typeof(T).Name; var client = new CloudTableClient(_account.TableEndpoint.AbsoluteUri, _account.Credentials); client.CreateTableIfNotExist(_tableName); } public IEnumerable<T> Retrieve() { return CreateQuery<T>(_tableName); } public void Create(T entity) { entity.CreatedOn = DateTime.Now; AddObject(_tableName, entity); SaveChanges(); } public void Update(T entity) { UpdateObject(entity); SaveChanges(); } public void Delete(T entity) { DeleteObject(entity); SaveChanges(); } } }
在完成了ITableServiceReporsitory接口和TableServiceRepository基类后,如果需要操作一个新的Table Entity,相关的数据访问代码实现起来就会非常简单。以相册部分的Album Entity为例,首先修改Album类,使其派生自TableServiceEntityBase基类。然后删除AlbumDataContext类并创建一个新的访问层类AlbumTableServiceRepository,它派生自TableServiceRepository基类并且实现了ITableServiceRepository接口,将Album类作为AlbumTableServiceRepository类的泛型参数。由于TableServiceRepository实现了所有的增删改查具体操作,所以这个派生类无须任何特殊的实现。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Aurora.Data.Entities; using Microsoft.WindowsAzure; namespace Aurora.Data.Repositories { public class AlbumTableServiceRepository : TableServiceRepository<Album>, ITableServiceRepository<Album> { public AlbumTableServiceRepository(CloudStorageAccount account) : base(account) { } } }
由于这里修改了数据访问层的接口,因此还需要相应修改ASP.NET MVC网站部分的调用代码。具体的修改内容这里就不再详细介绍了。这样,我们就完成了基于Repository模式的数据访问层,而在此基础上实现针对照片的操作就变得非常简单了。
参考
修改后的代码请参考ch009_aurora_task004_end.zip。
4.2.3.3 基于Repository基类实现照片管理的部分功能
基于Repository模式修改后的数据访问层对于实现其他Entity的增删改查功能变得非常方便。以照片管理为例,首先需要一个相册的详细信息页面,这个页面将会显示这个相册下面的照片信息。之后还需要在这个页面实现添加照片、修改照片和删除照片的链接及相应功能。由于照片本身将会保存在BLOB Service中,所以在这部分代码中暂时不会涉及照片的上传和显示。
首先定义照片Entity的属性。
· Partition Key:默认属性,仍旧以用户的E-mail作为Partition Key,这样保证同一用户的相册和照片信息都会保存在同一个存储节点上。
· Row Key:默认属性,仍旧以GUID作为Row Key。
· Timestamp:默认属性,无须人工管理。
· CreatedOn:基类属性,上传照片的时间,日期时间类型。
· AlbumRowKey:照片所在的相册Entity的Row Key,字符串类型。通过这个属性可以找到当前照片所在的相册信息。
· Name:照片的名称,字符串类型。
· ViewedCount:照片被查看的次数,每次打开这个照片的时候加一。同时此照片所在相册的查看次数也需要相应加一。
· URL:照片本身保存的地址,字符串类型。由于现在还没有实现上传照片的功能,所以这个属性暂时为空,未来这个属性将保存照片所在的BLOB URL。
之后基于这个Entity的属性设计实现对应的Photo类。和之前实现的Album类一样,它派生自TableServiceEntityBase基类,完成后的代码如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Aurora.Data.Entities { public class Photo : TableServiceEntityBase { public string AlbumRowKey { get; set; } public string Name { get; set; } public long ViewedCount { get; set; } public string URL { get; set; } public Photo(string memberEmail) : this(memberEmail, string.Empty) { } public Photo(string memberEmail, string albumRowKey) : base(memberEmail) { AlbumRowKey = albumRowKey; } public Photo() : base() { } } }
然后基于TableServiceRepository实现对于Photo Entity的操作类PhotoTableServiceRepository。这个操作类和刚才介绍的AlbumTableServiceRepository非常类似,只不过这一次使用Photo作为类的泛型参数,实现后的代码如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Aurora.Data.Entities; using Microsoft.WindowsAzure; namespace Aurora.Data.Repositories { public class PhotoTableServiceRepository : TableServiceRepository<Photo>, ITableServiceRepository<Photo> { public PhotoTableServiceRepository(CloudStorageAccount account) : base(account) { } } }
可以看到,针对Photo Entity的数据访问已经做好了。下面就可以开始实现添加、修改、删除以及浏览照片的具体操作界面了。由于大部分代码和相册的管理非常相似,所以这里只是简单介绍一下。
创建一个PhotoController并且添加Index方法负责展示指定相册中的所有照片;添加Create方法负责在指定相册中创建照片;添加Edit方法负责修改某个照片的信息;添加Delete方法负责删除某个照片。
对于Index方法,也就是浏览指定相册中照片的功能,还需要通过参数得到指定相册的Partition Key和Row Key。然后通过AlbumTableServiceRepository类找到当前的相册并取出对应的名称。之后通过刚刚写好的PhotoTableServiceRepository类获取这个相册下面的所有照片信息。完成后的代码如下所示。
[HttpGet] public ActionResult Index(string albumParitionKey, string albumRowKey) { // retrieve the album name from the table service var ablumRepo = new AlbumTableServiceRepository(CloudStorageAccount. FromConfigurationSetting("DataConnectionString")); ViewData["Album"] = ablumRepo.Retrieve() .Where(al => al.PartitionKey == albumParitionKey && al.RowKey == albumRowKey) .FirstOrDefault(); // retrieve the photo list from the album row key var photoRepo = new PhotoTableServiceRepository(CloudStorageAccount. FromConfigurationSetting("DataConnectionString")); var photos = photoRepo.Retrieve() .Where(p => p.PartitionKey == albumParitionKey && p.AlbumRowKey == albumRowKey) .ToList() .OrderBy(p => p.Name); return View(photos); }
提示
在上述代码中,我们使用了ViewData来缓存获取当前的相册信息,而在真正的产品开发中推荐使用专门的View Model类以获得强类型支持。
之后实现创建照片的相关操作。和创建相册类似,首先需要创建照片的界面,然后让用户通过这个界面输入照片的名字并上传照片文件。完成后的对应Action如下所示。
[HttpGet] public ActionResult Create(string albumParitionKey, string albumRowKey, string albumName) { var photo = new Photo(albumParitionKey, albumRowKey); ViewData["AlbumPartitionKey"] = albumParitionKey; ViewData["AlbumRowKey"] = albumRowKey; ViewData["AlbumName"] = albumName; return View(photo); }
接下来通过PhotoTableServiceRepository在Table Service中创建这个照片对应的Entity。由于照片的名字是可以重复的,所以这里只需要检测名字是否为空就可以了,然后调用数据访问层的Create方法。
[HttpPost] public ActionResult Create(Photo photo, string albumParitionKey, string albumRowKey, string albumName) { // validation if (string.IsNullOrWhiteSpace(photo.Name)) ModelState.AddModelError("Name", "Please input the name of this photo."); // TODO: validate the URL is not null if (ModelState.IsValid) { // create the photo entity into table service var repo = new PhotoTableServiceRepository(CloudStorageAccount. FromConfigurationSetting("DataConnectionString")); repo.Create(photo); // TODO: upload the photo into blob service return RedirectToAction("Index", new { albumParitionKey = albumParitionKey, albumRowKey = albumRowKey }); } else { return Create(albumParitionKey, albumRowKey, albumName); } }
图4-21就是完成后的照片列表页面,可以看到此时已经创建了一个照片的Entity,但是由于没有上传照片的功能,URL部分还是空的。
图4-21 完成后的照片列表页面
使用类似的方法完成对于照片的修改和删除操作,这里就不再详细介绍了。
到此为止,我们已经介绍了如何通过Windows Azure SDK提供的类库访问Table Service,包括如何创建Table,如何在Table中创建、修改和删除Entity的操作,以及如何使用LINQ方式对Entity进行查询的操作。
参考
完成后的代码ch010_aurora_task005_end.zip。
4.2.4 使用Table Service的事务操作
在查看照片这个功能中有一个业务需求,就是当用户打开了某张照片的详细信息页面时需要为这个照片的查看次数加一,同时还要为这个照片所在相册的查看次数加一。这两个操作需要通过事务处理,即照片和相册的查看次数都需要加一,否则(如果出现异常)都不加一。Table Service提供了有限的事务操作,即Entity Group Transactions,这里便需要通过这个功能来实现“照片和相册浏览次数同时加一”这个事务性操作需求。
Entity Group Transactions能够为Entity的添加、修改和删除加入事务支持,但却是一个非常有限的支持,想要使用Group Transaction必须要有如下的前提条件:
· 执行操作的Entity必须有相同的Partition Key。
· 在一个事务操作中,执行操作的Entity只能出现一次,并且针对一个Entity只能执行一个操作。也就是说在一个事务操作中先对Entity A执行了更新操作,之后又对这个Entity执行删除操作是不允许的。
· 最多支持1000个Entity参与一个事务,并且所有参与事务操作的Entity的总数据量不能大于4MB。
Entity Group Transactions可以通过Table Service的REST API执行,也可以通过Windows Azure SDK的类库调用执行。不知读者是否还记得在介绍TableServiceContext这个类的常用方法时提过SaveChanges这个函数(参考本书4.2.2节中使用Table Service Context访问Table Service部分内容)。对于带参数的SaveChanges重载函数,如果使用Batch这个提交原则,就表明这次操作使用了Entity Group Transactions事务功能。下面我们就来看一下如何使用事务操作功能来实现照片和相册浏览数累加的功能。
4.2.4.1 支持事务操作的数据访问层
由于当前的数据访问层都是基于强类型的Table Entity实现的,因此每个数据访问层类(AlbumTableServiceRepository)只能针对某一种Entity(Album)操作。这种设计是无法实现跨Entity事务操作的。所以需要使用之前提到的第二种类型的数据访问层,即Entity类型通过方法的泛型参数指定而不是通过类的泛型参数指定——通用数据访问层。
首先创建一个通用数据访问层接口:IDynamicTableServiceContext,代码如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Services.Client; namespace Aurora.Data { public interface IDynamicTableServiceContext { IEnumerable<T> Retrieve<T>() where T : TableServiceEntityBase; void Create<T>(T entity) where T : TableServiceEntityBase; void Update<T>(T entity) where T : TableServiceEntityBase; void Delete<T>(T entity) where T : TableServiceEntityBase; DataServiceResponse SaveChanges(); DataServiceResponse SaveChangesWithTransaction(); DataServiceResponse SaveChanges(SaveChangesOptions options); } }
这个接口和之前定义的ITableServiceRepository不尽相同。首先Entity类型是通过各个方法的泛型参数指定的,这样就可以通过这个接口对任何Table Entity进行操作了。其次,针对提交操作在接口中定义了三个SaveChanges方法,分别针对普通模式、事务模式和其他模式的提交,便于灵活调用。
接下来基于这个接口创建一个通用的数据访问层类,其实现方法和此前创建的TableService Repository类相类似,只不过在Create、Update和Delete方法中不再默认执行SaveChanges方法,而需要调用方显式调用SaveChanges进行数据提交。完成后的代码如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.WindowsAzure; using Microsoft.WindowsAzure.StorageClient; using System.Data.Services.Client; namespace Aurora.Data { public class DynamicTableServiceContext : TableServiceContext, IDynamicTableServiceContext { private CloudStorageAccount _account; private CloudTableClient _client; public DynamicTableServiceContext(string configName) : this(CloudStorageAccount.FromConfigurationSetting(configName)) { } public DynamicTableServiceContext(CloudStorageAccount account) : base(account.TableEndpoint.AbsoluteUri, account.Credentials) { _account = account; _client = new CloudTableClient(_account.TableEndpoint.AbsoluteUri, _account.Credentials); } private string CreateTableIfNotExist(Type typeOfEntity) { var tableName = typeOfEntity.Name; _client.CreateTableIfNotExist(tableName); return tableName; } public IEnumerable<T> Retrieve<T>() where T : TableServiceEntityBase { var tableName = CreateTableIfNotExist(typeof(T)); return CreateQuery<T>(tableName); } public void Create<T>(T entity) where T : TableServiceEntityBase { var tableName = CreateTableIfNotExist(typeof(T)); AddObject(tableName, entity); } public void Update<T>(T entity) where T : TableServiceEntityBase { var tableName = CreateTableIfNotExist(typeof(T)); UpdateObject(entity); } public void Delete<T>(T entity) where T : TableServiceEntityBase { var tableName = CreateTableIfNotExist(typeof(T)); DeleteObject(entity); } public new DataServiceResponse SaveChanges() { return SaveChanges(SaveChangesOptions.None); } public DataServiceResponse SaveChangesWithTransaction() { return SaveChanges(SaveChangesOptions.Batch); } public new DataServiceResponse SaveChanges(SaveChangesOptions options) { return base.SaveChanges(options); } } }
4.2.4.2 为查看照片操作加入事务支持
通过更新了的通用数据访问层,我们可以实现基于事务的Table Service操作。还是以查看照片为例,在PhotoController的Details方法中,原来更新照片和相册的访问次数操作是在执行了数据访问层的Update语句后,由基类执行了SaveChanges操作实现的数据提交,并没有事务支持。现在则需要通过新的通用数据访问层进行操作,获取当前查看的照片和相册信息。
[HttpGet] public ActionResult Details(string albumParitionKey, string albumRowKey, string photoRowKey) { var ctx = new DynamicTableServiceContext("DataConnectionString"); // retrieve the photo entity var photo = ctx.Retrieve<Photo>() .Where(p => p.PartitionKey == albumParitionKey && p.RowKey == photoRowKey) .FirstOrDefault(); var album = ctx.Retrieve<Album>() .Where(al => al.PartitionKey == albumParitionKey && al.RowKey == albumRowKey) .FirstOrDefault(); }
然后执行为照片和相册的访问次数加一的操作,并且调用Update方法进行更新。
[HttpGet] public ActionResult Details(string albumParitionKey, string albumRowKey, string photoRowKey) { var ctx = new DynamicTableServiceContext("DataConnectionString"); // retrieve the photo entity var photo = ctx.Retrieve<Photo>() .Where(p => p.PartitionKey == albumParitionKey && p.RowKey == photoRowKey) .FirstOrDefault(); var album = ctx.Retrieve<Album>() .Where(al => al.PartitionKey == albumParitionKey && al.RowKey == albumRowKey) .FirstOrDefault(); // update the viewed count of this photo and the album // this will be performed in one group transaction since the parition key is same photo.ViewedCount += 1; album.ViewedCount += 1; ctx.Update<Photo>(photo); ctx.Update<Album>(album); }
由于使用了新的数据访问层,虽然调用了Update方法,但是由于没有立即执行SaveChanges方法,因此修改的内容还没有提交到Table Service中。接下来调用SaveChangesWithTransaction方法,将上述操作在同一个事务中处理。
[HttpGet] public ActionResult Details(string albumParitionKey, string albumRowKey, string photoRowKey) { ... ... // update the viewed count of this photo and the album // this will be performed in one group transaction since the parition key is same photo.ViewedCount += 1; album.ViewedCount += 1; ctx.Update<Photo>(photo); ctx.Update<Album>(album); ctx.SaveChangesWithTransaction(); // return view ViewData["AlbumPartitionKey"] = albumParitionKey; ViewData["AlbumRowKey"] = albumRowKey; return View(photo); }
上面的代码就是基于事务处理的更新操作了。在本地模拟器中启动应用程序,进入某个相册,在照片列表中点击某张照片前面的Details链接进入照片详细页面,可以看到照片的View Count属性增加了。之后返回相册列表,可以看到这个相册的View Count也相应地增加了。
为了测试事务操作的特性,在执行SaveChangesWithTransaction语句的地方打上断点,然后再次点击这个照片的Details链接。如图4-22所示,在Visual Studio中手动跳过SaveChanges WithTransaction语句,可以看到,虽然代码中执行了更新照片和相册的Update操作,但是由于没有执行SaveChangesWithTransaction方法,因此照片和相册的访问值都没有更新。
图4-22 在调试模式下跳过保存操作
现在的代码中有基于两种数据访问层实现的代码,由于支持事务处理的通用数据访问层可以完全覆盖之前的强类型数据访问层,所以可以将原来的访问层代码删除掉,使用我们新的代码进行操作,这里就不再详细介绍了。
参考
完成后的代码请参考ch011_aurora_task006_end.zip。
4.2.4.3 为删除相册加入事务操作
和相册照片浏览次数累加的逻辑类似,在删除相册的时候,应用程序应该将指定相册内的所有照片连同相册一并删除,而且这个操作也应该是基于事务处理的。因此可以使用类似于前面处理照片操作的方式来修改此处的代码。
唯一不同的是,在删除照片的时候需要遍历当前相册内的所有Photo Entity,然后对于每一个Photo Entity执行删除操作,最后再执行SaveChangesWithTransaction操作。具体代码如下所示。
[HttpPost] public ActionResult Delete(Album album) { var ctx = new DynamicTableServiceContext("DataConnectionString"); // delete the album var albumToDelete = ctx.Retrieve<Album>() .Where(al => al.PartitionKey == album.PartitionKey && al.RowKey == album.RowKey) .FirstOrDefault(); ctx.Delete(albumToDelete); // delete the photots in this album var blobResult = true; var photosToDelete = ctx.Retrieve<Photo>() .Where(p => p.PartitionKey == album.PartitionKey && p.AlbumRowKey == album.RowKey) .ToList(); foreach (var photoToDelete in photosToDelete) { if (blobResult) { ctx.Delete<Photo>(photoToDelete); // TODO: delete the photo in blob service and update the blobResult } } // perform the transaction if (blobResult) { ctx.SaveChangesWithTransaction(); } return RedirectToAction("Index"); }
参考
完成后的代码请参照ch012_aurora_task007_end.zip。
至此,我们已经介绍了Table Service的主要功能,以及如何通过Windows Azure SDK提供的类库访问Table Service。其中包括如何定义Table Service所要使用的Entity以及访问Table Service的类TableServiceContext。随后又介绍了两种不同形式的数据访问层:基于类泛型参数的数据访问层和基于方法泛型参数的数据访问层,以及如何基于后者使用Entity Group Transaction功能。
在Aurora项目中,对于相册和照片的管理功能已经全部完成了,但是似乎还有一个最重要的功能有实现——上传照片。虽然Table Service也允许保存二进制类型的Property,可以将照片信息作为一个属性保存在Table Service中,但是由于Windows Azure平台对Entity的大小是有限制的,所以更好的解决方案是将照片上传到专门为文件存储优化的BLOB Service中。接下来,我们将介绍BLOB Service的特点及使用方法,最终完成照片上传的功能。