2011年11月1日

Windows Azure 儲存體–Table

2012/11/06 更新 - 感謝Wifi哥的指教,修正了程式碼的錯誤,由原本的DataKeyNames="PartitionKey"改成DataKeyNames="PartitionKey,RowKey"。

說到Table,大家就會直接聯想到DB的Table,而Windows Azure的其中一個儲存體也叫Table,雖然也叫Table,但和DB的Table不太同,Windows Azure的Table沒辦法像DB一樣,可以設定多個Table的關聯,當然也沒辦法自己去設定PK、FK等等這類似的東西,如果你真的要將資料庫搬移到Windows Azure,那你應該選擇的是SQL Azure ( SQL Server的雲端版本 ),而不是將原本的資料庫內容搬到Windows Azure喔!,這點要注意一下喔!接下來,我們開始介紹Table。

Table架構

如下圖,其實Azure的儲存體,都是由Account ( 帳號 ) 開始,而一個Account可以有多個Table,每個Table又會包含許多的Entity ( 實體 ),每個Entity會有許多的Properties( 屬性 ),其中每個Entity一定會有PartitionKey、RowKey、Timestamp這三個屬性。

image

這裡,我們針對剛剛上面看到的一堆名詞,做個解釋。

  • Storage Account - 所有的Table都是透過此帳號進行存取,他也是整個架構的起點,一個帳號可以有非常多的Table。
  • Table - 其實也是一個實體,它包含在Account裡面,而且一個Account可以建立很多個Table。
  • Entity ( Row ) - 實體,也很類似於Row ( 行 ),他是存於Table裡面的一個基本的資料類型,一個實體會有一組Properties ( 屬性 ),其中的"PartitionKey"和"RowKey"會組成唯一鍵。
  • Property (Column) - 屬性,也很類似於Column ( 欄位 ),他代表著一個實體裡面的其中一個值,而且他支援豐富的型別,另外要注意的是,他有區分大小寫。
  • PartitionKey - 每個實體都會有這個屬性,而這個屬性擁有類似分類的功能,例如,我們可能會把PartitionKey設為Car、機車,來分類。
  • RowKey - 每個實體都會有這個屬性,我們會利用這個屬性來區別PartitonKey分出來類別裡面唯一的實體。
  • Timestamp - 時間戳記,由系統紀錄此Entity何時被修改過。

其中PartitionKey和RowKey比較難讓人懂,其實PratitionKey就等於紀錄這個Entity是屬於哪一個類別,如下圖,假設有許多文章,我們就可以設定好幾筆的資料Partition Key為Examples Doc來進行分類 ( Partition 1 ),另外,又可以設定為FAQ Doc類 ( Partition  ),而同一個類(同一個Partition Key)會有許多的Entity,於是就用RowKey來代表唯一的Entity;所以當Partition Key加上Row Key,就可組合成獨一無二的Key,讓我們在茫茫的大Table找到我們想要的,此外,Partition Key也涉及了搜尋效能,所以分類的時候,要想清楚。

image

此外,關於每個屬性的型態,可以參考此表。

Property Type Details
Binary An array of bytes up to 64 KB in size.
Bool A Boolean value.
DateTime A 64-bit value expressed as UTC time. The supported range of values is 1/1/1601 to 12/31/9999.
Double A 64-bit floating point value.
GUID A 128-bit globally unique identifier.
Int A 32-bit integer.
Int64 A 64-bit integer.
String A UTF-16-encoded value. String values may be up to 64 KB in size.

最後,因為是雲端儲存體,所以如果我們要取得Table,可想而知,就一定是使用Url來當作識別的位置啦!如下。

http://<account>.table.core.windows.net/<table>/

來寫程式吧!!

接下來,我們就來撰寫程式看看吧,我相信大家應該也沒有那麼多的錢可以租用Windows Azure服務,老實說,小弟我也沒那麼多錢,所以我的範例都是在本機上面做測試。

首先,要寫Windows Azure,必須先裝SDK,如果還沒裝的可以參考這篇。接下來就在Visual Studio裡面選擇Windows Azure專案。

image_thumb5[1]

到這邊,就可以選擇Web Role,但小弟我不喜歡在這邊選擇,會利用後續再增加的做法,有興趣的可以參考這篇,如果只想要簡單Demo,也可以直接在這邊選擇ASP.NET Web Role ( 中文翻角色 )。

image_thumb16

接下來,我們先來看看最後寫完的範例程式執行起來的樣子吧;基本上就是長這樣,只要在姓名與住址的地方填入資料,新增後就會在Grid裡面增加,如果要刪除,就可以自行刪除檔案。

image

這次的這個範例,裡面會有許多的檔案,所以我先把檔案列出來;主要會撰寫到的有:

  • Customer.cs - 此為Cusomter類別,裡面會放置姓名和地址屬性。
  • TableContext.cs - 此定義為Customers這個Table。
  • DataSource - 存取資料用的。
  • Default.aspx - 主要的Web Role。

image

既然是物件導向,那我們就寫核心的Customer吧;Customer算是滿簡單的,但要注意一下,Customer必須要繼承TableServiceEntity,別忘了,Azure Table的Entity有一些必定要有的屬性( 例如:PartitionKey ),所以並不是所有東西都可以往上丟,我們必須先繼承TableServiceEntity;然後我們於建構式中將PartitionKey傳入Customers,而RowKey的部分,則利用Guid.NewGiud()來產生唯一碼。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.WindowsAzure.StorageClient;

namespace WebApplicationRole
{
    //需要繼承TableServiceEntity
    public class Customer : TableServiceEntity
    {
        public Customer(string partitionKey, string rowKey):base(partitionKey,rowKey)
        {
        }

        public Customer()
            : this("Customers", Guid.NewGuid().ToString())
        {
        }

        public string name { get; set; }
        public string Address { get; set; }

    }
}

這樣子就完成了Customer,接下我們來設計TableContext,TableContext必須繼承於TableServiceContext,理由和TableServiceEntity一樣,而建構式的部分,則必須傳入bassAddress,和驗證,如果我們取得了Account,我們就可以利用Account的TableEndpoint方法來取得bassAddress,同樣的,也可以利用Account的Credentials來取得驗證;其次,我們定義了Customers的這個屬性,並且利用get來取得所有的Customer,需要注意的是,這裡會使用TableServiceContext的CreateQuery方法來傳回所有的Customer。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.WindowsAzure.StorageClient;
using Microsoft.WindowsAzure;

namespace WebApplicationRole
{
    public class TableContext : TableServiceContext
    {
        public TableContext(string baseAddress,StorageCredentials credentials):base(baseAddress,credentials)
        {
        }

        //定義Table名稱
        public const string TableName = "Customers";

        //取得Customers這個Table的所有資料
        public IQueryable<customer> Customers
        {
            get
            {
                //需要加入System.Data.Services.Client。
                //利用CreateQuery來取得Customers裡面所有的Customer
                return this.CreateQuery<customer>(TableName);
            }
        }
    }
}

完成了TableContext,我們就可以開始撰寫DataSouce.cs了,顧名思義,就是撰寫取得資料來源的類別;這個類別會定義查詢、新增和刪除;我們會利用建構子去建構連線,並且取得Account和TableContext( 可以想像成Table ),後續我們才能繼續處理;select的地方比較容易理解,利用Linq的方式來取得Customer;而Delete則有個地方要注意,我們會用AttachTo來追蹤支援,這樣才會知道要刪除的位置,刪除後再利用SaveChanges去將資料寫回雲端儲存體;而insert的部分,比較簡單,利用AddObject將Customer加到TableContext裡面,一樣使用SaveChanges來進行變更;而AddObject和AttachTo,必須給他們實體的名稱,也就是第一個參數,而這裡的名稱就是Customers。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
//要手動加這三行
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;
using Microsoft.WindowsAzure.ServiceRuntime;

namespace WebApplicationRole
{
    public class DataSource
    {
        private TableContext _ServiceContext = null;

        public DataSource()
        {
            //取得Account
            var storageAccount = CloudStorageAccount.DevelopmentStorageAccount;
            //取得TableContext
            _ServiceContext = new TableContext(storageAccount.TableEndpoint.ToString(), storageAccount.Credentials);
            //假如沒有此Table,則建立此Table
            storageAccount.CreateCloudTableClient().CreateTableIfNotExist(TableContext.TableName);
        }

        public IEnumerable<customer> Select()
        {
            var results = from c in _ServiceContext.Customers
                          select c;
            return results;
        }

        public void Delete(Customer itemToDelete)
        {
            //追蹤資源,這樣才知道位置在哪裡。
            _ServiceContext.AttachTo(TableContext.TableName, itemToDelete, "*");
            _ServiceContext.DeleteObject(itemToDelete);
            _ServiceContext.SaveChanges();
        }

        public void Insert(Customer newItem)
        {
            _ServiceContext.AddObject(TableContext.TableName, newItem);
            _ServiceContext.SaveChanges();
        }
    }
}

最後,我們就可以編輯HTML了,我們利用ASP.NET的ObjectDataSource來指向剛剛設定好的DataSource,並且讓Grid Bind再一起。

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplicationRole._Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1" runat="server">
    <title>Table Demo</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:GridView id="DataView" DataSourceId="TableData" DataKeyNames="PartitionKey,RowKey" 
        AllowPaging="False" AutoGenerateColumns="True" Runat="server" >
        <Columns>
            <asp:CommandField ShowDeleteButton="true"  />
        </Columns>
    </asp:GridView>    
        <br />        
    <asp:FormView id="frmAdd" DataSourceId="TableData" DefaultMode="Insert" Runat="server" Width="403px">
        <InsertItemTemplate>
            <asp:Label id="nameLabel" Text="姓名:" AssociatedControlID="nameBox" Runat="server" />
            <asp:TextBox id="nameBox" Text='<%# Bind("Name") %>' Runat="server" />
            <asp:Label id="addressLabel" Text="住址:" AssociatedControlID="addressBox" Runat="server" />
            <asp:TextBox id="addressBox" Text='<%# Bind("Address") %>' Runat="server" />
            <asp:Button id="insertButton" Text="新增" CommandName="Insert" Runat="server"/>
        </InsertItemTemplate>
    </asp:FormView>
    <%-- Data Sources --%>
    <asp:ObjectDataSource runat="server" ID="TableData"  TypeName="WebApplicationRole.DataSource"
        DataObjectTypeName="WebApplicationRole.Customer" 
        SelectMethod="Select" DeleteMethod="Delete" InsertMethod="Insert">    
    </asp:ObjectDataSource>

    </div>
    </form>
</body>
</html>

最後是Default.aspx.cs的程式碼,這就沒甚麼好講的了。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace WebApplicationRole
{
    public partial class _Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            DataSource dataSource = new DataSource();
        }
    }
}

我們這又就完成了程式,如果有寫過Entity Framework等的人,可能會覺得比較熟悉,但如果沒寫過Entity Framework的人,可能有許多地方搞不太清楚,有興趣的話,也可以去翻一翻關於Entity Framework的書籍,或是Linq的書籍,我相信會有很大的幫助。

參考資料

3 則留言:

  1. 作者已經移除這則留言。

    回覆刪除
  2. GridView 中的 DataKeyNames 屬性, 是不是應設為 DataKeyNames="PartitionKey,RowKey", 才能夠正確地刪除目標列呢?

    回覆刪除
  3. Winf哥 您好!!
    很抱歉,一直沒看到這個留言,到今天才發現;
    您說的沒錯DataKeyNames="PartitionKey,RowKey"要設定成這樣才會正確抓到值,很感謝您的糾正與指教!!未來如果有發現問題,也請和小弟說喔!!
    非常感謝....
    ( 後來看了一下原始碼....原來這邊的範例,當初貼到另外一個錯誤的....Orz.... )

    回覆刪除