Table Storage对分页的支持

大家可能知道WCF Data Services最新版提供了server paging的功能,意即在服务期端对数据进行分页,从而限制传回客户端的数据量。那么Windows Azure table storage是否提供分页功能呢?

Windows Azure table storage本身就限制了客户端一次性可以访问的数据量不能超过100条。当你需要访问的数据量超过100条时,只有前100条数据会被返回至客户端。同时response会包含一个x-ms-continuation-NextPartitionKey和x-ms-continuation- NextRowKey (通称continuation token),它代表着第101条数据的key。在后续的请求中,你可以将之前或取得continuation token通过query string传递给table storage,从而取得第101至第200条数据。

同理,当你使用take这个query string强制限制返回的数据量时,continuation token也会被包含于response中。

使用过新版本WCF Data Services的server paging功能的用户应该清楚如何在客户端程序中控制continuation token,从而对数据进行分页。在table storage中,你也可以使用类似的方式。只不过用法稍有不同。有关continuation token的基本用法,请参考https://blog.smarx.com/posts/windows-azure-tables-expect-continuation-tokens-seriously

然而你或许很快会发现server paging的局限性。是的,server paging的作用主要在于限制客户端一次性可以访问的数据量,而不在于帮助客户端构建起分页导航的功能。通过continuation token,你可以方便地取得下一个页面的数据。但是如果你要反过来取得之前一个页面的数据,就不那么简单了……

通常客户端的分页导航功能需要client paging来实现。例如,在WCF Data Services中,你可以通过skip和take这样的query string来要求服务返回从特定的起始点到特定的终结点的数据。然后很遗憾,table storage是不支持skip的,于是也就无法完整地使用client paging。

一个部分的解决方法是,你必须手工保存一个continuation token的列表。只要有了continuation token,我们就可以要求table storage从特定的记录开始返回。至于如何保存continuation token的列表,方式是多种多样的。以下演示如何将历史记录信息存储在cookie中,从而在ASP.NET中实现分页导航功能。

MVC的controller(控制导航逻辑):

              public ActionResult Index()

              {

                     // 使用JSON将continuation token的历史信息存储到cookie中。

                     string historyString = null;

                     if (Request.Cookies["history"] != null)

                     {

                           historyString = Request.Cookies["history"].Value;

                     }

                     using (MemoryStream stream = new MemoryStream())

                     {

                           using (StreamWriter writer = new StreamWriter(stream))

                           {

                                  writer.Write(historyString);

                                  writer.Flush();

                                  DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(List<string>));

                                  List<string> history = null;

                                  try

                                  {

                                         stream.Position = 0;

                                         // 从JSON返序列化。

                                         history = serializer.ReadObject(stream) as List<string>;

                                  }

                                  catch

                                  {

                                  }

                                  if (history == null)

                                  {

                                         history = new List<string>();

                                  }

                                  StorageCredentialsAccountAndKey credential = new StorageCredentialsAccountAndKey("[account]", "[key]");

                                 CloudStorageAccount account = new CloudStorageAccount(credential, false);

                                  CloudTableClient tableClient = new CloudTableClient(account.TableEndpoint.ToString(), account.Credentials);

                                  CustomerServiceContext ctx = new CustomerServiceContext(account.TableEndpoint.ToString(), account.Credentials);

 

                                  var query = (DataServiceQuery<Customer>)(ctx.Customer.Take(10));

 

                                  // 将currentPage传入query string。

 

                                  string currentPage = Request["currentPage"];

 

                                  int currentPageNumber = 0;

 

                                  if (!string.IsNullOrEmpty(currentPage))

                                  {

 

                                         currentPageNumber = int.Parse(currentPage);

 

                                  }

 

                                // action表明用户想要导航至上一页,从历史记录中取得对应的continuation token。

 

                                  string action = Request["action"];

 

                                  if (action == "previous")

                                  {

 

                                         currentPageNumber--;

 

                                         if (currentPageNumber != 0)

                                         {

 

                                                string[] tokens = history[currentPageNumber].Split('/');

 

                                                var partitionToken = tokens[0];

 

                                                var rowToken = tokens[1];

 

                                                query = query

 

              .AddQueryOption("NextPartitionKey", partitionToken)

 

              .AddQueryOption("NextRowKey", rowToken);

 

                                         }

 

                                  }

 

                                // action表明用户想要导航至上一页,从历史记录中取得对应的continuation token。

                                  else if (action == "next")

                                  {

 

                                         currentPageNumber++;

 

                                         string continuation = Request["ct"];

 

                                         if (continuation != null)

                                         {

 

                                                string[] tokens = continuation.Split('/');

 

                                                var partitionToken = tokens[0];

 

                                                var rowToken = tokens[1];

 

                                                query = query

 

                                                      .AddQueryOption("NextPartitionKey", partitionToken)

 

                                                       .AddQueryOption("NextRowKey", rowToken);

 

                                         }

 

                                  }

 

                                  var res = query.Execute();

 

                                  var qor = (QueryOperationResponse)res;

 

                                  string nextPartition = null;

 

                                  string nextRow = null;

 

                                  qor.Headers.TryGetValue("x-ms-continuation-NextPartitionKey", out nextPartition);

 

                                  qor.Headers.TryGetValue("x-ms-continuation-NextRowKey", out nextRow);

 

                                  if (nextPartition != null && nextRow != null)

                                  {

 

                                        ViewData["continuation"] = string.Format("{0}/{1}", nextPartition, nextRow);

 

                                  }

 

                                  var result = res.ToList();

 

                                  var firstEntity = result.First();

 

                                  // 将新的continuation token添加到历史记录中,如果它尚不存在的话……

 

                                  string historyEntry = string.Format("{0}/{1}", firstEntity.PartitionKey, firstEntity.RowKey);

 

                                  if (!history.Contains(historyEntry))

                                  {

 

                                         history.Add(historyEntry);

 

                                  }

 

                                  // 序列化成JSON。

                                  stream.Position = 0;

                                  serializer.WriteObject(stream, history);

                                  stream.Position = 0;

                                  using (StreamReader reader = new StreamReader(stream))

                                  {

                                         historyString = reader.ReadToEnd();

                                         Response.Cookies.Add(new HttpCookie("history", historyString));

                                  }

                                  ViewData["currentPageNumber"] = currentPageNumber;

 

                                  ViewData["result"] = result;

 

                                  return View();

                           }

                     }

              }

 

MVC的view(显示Previous/Next导航):

            <a href='?currentPage=<%= ViewData["currentPageNumber"] %>&action=previous'>Previous</a> 

            <a href='?ct=<%= ViewData["continuation"] %>&currentPage=<%= ViewData["currentPageNumber"] %>&action=next'>Next</a>

 

当然,这种方式也是有局限性的。例如,用户无法从中间某个页面开始访问数据,必须从第一页开始访问,从而能够建起一个continuation token的历史记录。由于table storage不支持count,我们也无法显示总共有几个页面。这是server paging的局限性,通常server paging被用于限制客户端一次性最多能访问多少数据,从而避免一些可能因数据量太多而发生的问题(诸如连接超时)。要构建起完整的分页导航功能,还需要client paging的支持。然而在目前table storage只支持server paging的情况下,上述解决方案让你能够构建起基本的分页导航功能。