共用方式為


在架構中快取資料 (VB)

作者:Scott Mitchell

下載 PDF

在先前的教學中,我們學習如何在表示層應用快取。 在本教學課程中,我們學習如何利用分層架構在業務邏輯層快取資料。 我們透過擴展架構以包含快取層來實現這一點。

簡介

正如我們在前面的教學課程中看到的,快取 ObjectDataSource 的資料就像設定幾個屬性一樣簡單。 不幸的是,ObjectDataSource 在表示層應用快取,這將快取策略與 ASP.NET 頁面緊密耦合。 建立分層架構的原因之一是允許打破這種耦合。 例如,業務邏輯層將業務邏輯與 ASP.NET 頁面分離,而資料存取層將資料存取細節分離。 這種業務邏輯和資料存取細節的解耦是首選,部分原因是它使系統更具可讀性,更易於維護,並且更改起來更靈活。 它還允許領域知識和分工,在表示層上工作的開發人員不需要熟悉資料庫的詳細資訊即可完成工作。 將快取策略與表示層解耦也能帶來類似的好處。

在本教學課程中,我們將增強我們的架構,以包含採用我們的快取策略的快取層 (或簡稱 CL)。 快取層將包含一個類別,ProductsCL 類別透過 GetProducts()GetProductsByCategoryID(categoryID) 等方法提供對產品資訊的訪問,在呼叫該類別時,將首先嘗試從快取中擷取資料。 如果快取為空,這些方法將呼叫 BLL 中的對應 ProductsBLL 方法,進而從 DAL 取得資料。 這些 ProductsCL 方法在傳回之前會快取從 BLL 擷取的資料。

如圖 1 所示,CL 位於表示層與業務邏輯層之間。

快取層 (CL) 是我們架構中的另一層

圖 1:快取層 (CL) 是我們架構中的另一層

第 1 步:建立快取層類別

在本教學課程中,我們將建立一個非常簡單的 CL,其中包含一個只有少數方法的類別 ProductsCL。 為整個應用程式建立完整的快取層需要建立 CategoriesCLEmployeesCLSuppliersCL 類別,並在這些快取層類別中為 BLL 中的每個資料存取或修改方法提供一個方法。 與 BLL 和 DAL 一樣,快取層理想情況下應作為單獨的類別庫項目實現;但是,我們將把它作為 App_Code 資料夾中的類別來實現。

為了更清晰地將 CL 類別與 DAL 和 BLL 類別分開,讓我們在 App_Code 資料夾中建立一個新的子資料夾。 右鍵點擊解決方案資源管理器中的 App_Code 資料夾,選擇“新資料夾”,並將新資料夾命名為 CL。 建立此資料夾後,向其中新增一個名為 ProductsCL.vb

新增名為 CL 的新資料夾和名為 ProductsCL.vb 的類別

圖 2:新增名為 CL 的新資料夾和名為 ProductsCL.vb 的類別

ProductsCL 類別應包含與其對應的業務邏輯層類別 (ProductsBLL) 中相同的資料存取和修改方法集。 我們不是建立所有這些方法,而是在這裡建立幾個方法來感受 CL 使用的模式。 特別是,我們將在步驟 3 中新增 GetProducts()GetProductsByCategoryID(categoryID) 方法,並在步驟 4 中新增 UpdateProduct 重載。 您可以隨意加入剩餘的 ProductsCL 方法和 CategoriesCLEmployeesCLSuppliersCL 以及類別。

步驟 2:讀取和寫入資料快取

前面教學中探討的 ObjectDataSource 快取功能在內部使用 ASP.NET 資料快取來儲存從 BLL 擷取的資料。 也可以透過 ASP.NET 頁面程式碼隱藏類別或 Web 應用程式體系結構中的類別以程式設計方式存取資料快取。 若要從 ASP.NET 頁面的程式碼隱藏類別讀取和寫入資料快取,請使用下列模式:

' Read from the cache
Dim value as Object = Cache("key")
' Add a new item to the cache
Cache("key") = value
Cache.Insert(key, value)
Cache.Insert(key, value, CacheDependency)
Cache.Insert(key, value, CacheDependency, DateTime, TimeSpan)

Cache 類別 Insert 方法 有許多重載。 Cache("key") = valueCache.Insert(key, value) 是同義詞,並且都使用指定的鍵將項目添加到快取,而沒有定義的到期時間。 通常,我們希望在將項目新增至快取時指定過期時間,可以作為依賴項、基於時間的過期時間,或兩者兼而有之。 使用其他 Insert 方法重載之一來提供基於依賴性或基於時間的到期資訊。

快取層的方法需要先檢查請求的資料是否在快取中,如果是,則從快取中傳回資料。 如果請求的資料不在快取中,則需要呼叫適當的 BLL 方法。 它的返回值應該被快取然後返回,如下面的序列圖所示。

快取層的方法從快取返回資料 (如果可用)

圖 3:快取層的方法從快取返回資料(如果可用)

圖 3 中描述的序列是使用以下模式在 CL 類別中完成的:

Dim instance As Type = TryCast(Cache("key"), Type)
If instance Is Nothing Then
    instance = BllMethodToGetInstance()
    Cache.Insert(key, instance, ...)
End If
Return instance

這裡,Type 是儲存在快取中的資料類別型,例如 key 是唯一標識快取 Northwind.ProductsDataTable 項目的鍵。 如果具有指定的項目不在快取中,則實例將在快取中,並且將 Nothing 從適當的 BLL 方法擷取資料並將新增至快取。 當到達時,實例包含對資料的引用,無論是來自快取還是從 BLL 中提取 Return instance

從快取存取資料時請務必使用上述模式。 下面的模式乍看之下似乎是等效的,但其中包含引入競爭條件的細微差別。 競爭條件很難調試,因為它們偶爾會顯現出來並且很難重現。

If Cache("key") Is Nothing Then
    Cache.Insert(key, BllMethodToGetInstance(), ...)
End If
Return Cache("key")

第二個不正確的程式碼片段的差異在於,資料快取不是在局部變數中儲存對快取項目的引用,而是直接在 Return 條件陳述式 。 想像一下,當到達此程式碼時,Cache("key") 不是 Nothing,但在到達 Return 陳述式之前,系統從快取中逐出。 在這種罕見的情況下,程式碼將傳回 Nothing 而不是預期類別型的物件。

注意

資料快取是線程安全的,因此您不需要為簡單的讀取或寫入同步線程存取。 但是,如果您需要對快取中的資料執行多個需要原子操作的操作,則您需要負責實作鎖定或其他一些機制來確保執行緒安全。 有關詳細資訊,請參閱同步對 ASP.NET 快取的存取

可以使用以下 Remove 方法以程式設計方式從資料快取中逐出項目

Cache.Remove(key)

步驟 3:從 ProductsCL Class 返回產品訊息

在本教學課程中,我們將實作兩種從 ProductsCL 類別傳回產品資訊的方法:GetProducts()GetProductsByCategoryID(categoryID)。 與業務邏輯層中的 ProductsBL 類別一樣,CL 中的 GetProducts() 方法將所有產品的資訊作為 Northwind.ProductsDataTable 物件傳回,同時 GetProductsByCategoryID(categoryID) 傳回指定類別的所有產品。

以下程式碼顯示了 ProductsCL 類別中的部分方法:

<System.ComponentModel.DataObject()> _
Public Class ProductsCL
    Private _productsAPI As ProductsBLL = Nothing
    Protected ReadOnly Property API() As ProductsBLL
        Get
            If _productsAPI Is Nothing Then
                _productsAPI = New ProductsBLL()
            End If
            Return _productsAPI
        End Get
    End Property
    <System.ComponentModel.DataObjectMethodAttribute _
    (DataObjectMethodType.Select, True)> _
    Public Function GetProducts() As Northwind.ProductsDataTable
        Const rawKey As String = "Products"
        ' See if the item is in the cache
        Dim products As Northwind.ProductsDataTable = _
            TryCast(GetCacheItem(rawKey), Northwind.ProductsDataTable)
        If products Is Nothing Then
            ' Item not found in cache - retrieve it and insert it into the cache
            products = API.GetProducts()
            AddCacheItem(rawKey, products)
        End If
        Return products
    End Function
    <System.ComponentModel.DataObjectMethodAttribute _
        (DataObjectMethodType.Select, False)> _
    Public Function GetProductsByCategoryID(ByVal categoryID As Integer) _
        As Northwind.ProductsDataTable
        If (categoryID < 0) Then
            Return GetProducts()
        Else
            Dim rawKey As String = String.Concat("ProductsByCategory-", categoryID)
            ' See if the item is in the cache
            Dim products As Northwind.ProductsDataTable = _
                TryCast(GetCacheItem(rawKey), Northwind.ProductsDataTable)
            If products Is Nothing Then
                ' Item not found in cache - retrieve it and insert it into the cache
                products = API.GetProductsByCategoryID(categoryID)
                AddCacheItem(rawKey, products)
            End If
            Return products
        End If
    End Function
End Class

首先,注意應用於類別和方法的 DataObjectDataObjectMethodAttribute 屬性。 這些屬性向 ObjectDataSource 精靈提供訊息,指示哪些類別和方法應出現在精靈的步驟中。 由於將從表示層中的 ObjectDataSource 存取 CL 類別和方法,因此我新增了這些屬性來增強設計時體驗。 有關這些屬性及其效果的更全面描述,請參閱建立業務邏輯層教學

GetProducts()GetProductsByCategoryID(categoryID) 方法中,從 GetCacheItem(key) 方法傳回的資料被指派給局部變數。 我們稍後將討論 GetCacheItem(key) 方法,它根據指定的從快取中返回特定的項目。 如果在快取中沒有找到這樣的資料,則從相應的 ProductsBLL 類別方法中擷取,然後使用 AddCacheItem(key, value) 方法新增到快取中。

GetCacheItem(key)AddCacheItem(key, value) 方法分別與資料快取、讀取和寫入值介面。 GetCacheItem(key) 方法是兩者中較簡單的方法。 它只是使用傳入的從 Cache 類別傳回值:

Private Function GetCacheItem(ByVal rawKey As String) As Object
    Return HttpRuntime.Cache(GetCacheKey(rawKey))
End Function
Private ReadOnly MasterCacheKeyArray() As String = {"ProductsCache"}
Private Function GetCacheKey(ByVal cacheKey As String) As String
    Return String.Concat(MasterCacheKeyArray(0), "-", cacheKey)
End Function

不使用提供的GetCacheItem(key)值,而是呼叫 方法,GetCacheKey(key) 方法傳回前綴為 ProductsCache- 的。 保存字串 ProductsCache 的 MasterCacheKeyArray 也被 AddCacheItem(key, value) 方法使用,我們稍後會看到。

從 ASP.NET 頁的程式碼隱藏 Page 類別中,可以使用類別的Cache屬性存取資料快取,並允許使用類似 Cache("key") = value 的語法,如步驟 2 所述。 從架構內的類別中,可以使用 HttpRuntime.CacheHttpContext.Current.Cache 來存取資料快取。 Peter Johnson 的部落格文章 HttpRuntime.Cache vs. HttpContext.Current.Cache 指出使用而不是 HttpRuntimeHttpContext.Current 具有輕微的效能優勢。因此,使用 ProductsCLHttpRuntime

注意

如果您的體系結構是使用類別庫專案實現的,那麼您將需要新增對 System.Web 組件的參考才能使用 HttpRuntimeHttpContext 類別。

如果在快取中沒有找到該項目,則 ProductsCL 類別的方法會從 BLL 取得資料並使用 AddCacheItem(key, value) 方法將其新增至快取。 要為快取添加值,我們可以使用以下程式碼,該程式碼使用 60 秒的過期時間:

Const CacheDuration As Double = 60.0
Private Sub AddCacheItem(ByVal rawKey As String, ByVal value As Object)
    DataCache.Insert(GetCacheKey(rawKey), value, Nothing, _
        DateTime.Now.AddSeconds(CacheDuration), _
        System.Web.Caching.Cache.NoSlidingExpiration)
End Sub

DateTime.Now.AddSeconds(CacheDuration) 指定基於時間的到期日 (未來 60 秒),同時 System.Web.Caching.Cache.NoSlidingExpiration 表示沒有滑動到期日。 雖然 Insert 方法重載具有絕對到期日和滑動到期日的輸入參數,但您只能提供兩者之一。 如果您嘗試同時指定絕對時間和時間跨度,Insert 方法將引發 ArgumentException 異常。

注意

目前 AddCacheItem(key, value) 方法的實作存在一些缺陷。 我們將在步驟 4 中解決並克服這些問題。

第四步:透過架構修改資料時使快取失效

除了資料擷取方法之外,快取層還需要提供與 BLL 相同的方法來插入、更新和刪除資料。 CL的資料修改方法不會修改快取的資料,而是呼叫BLL對應的資料修改方法,然後使快取失效。 正如我們在前面的教學課程中所看到的,這與 ObjectDataSource 在啟用其快取功能並呼叫其 InsertUpdateDelete 方法時所應用的行為相同。

以下 UpdateProduct 重載說明如何在 CL 中實作資料修改方法:

<DataObjectMethodAttribute(DataObjectMethodType.Update, False)> _
Public Function UpdateProduct(productName As String, _
    unitPrice As Nullable(Of Decimal), productID As Integer) _
    As Boolean
    Dim result As Boolean = API.UpdateProduct(productName, unitPrice, productID)
    ' TODO: Invalidate the cache
    Return result
End Function

呼叫適當的資料修改業務邏輯層方法,但在返回其回應之前,我們需要使快取失效。 不幸的是,使快取失效並不簡單,因為 ProductsCL 類別 GetProducts()GetProductsByCategoryID(categoryID) 方法各自使用不同的鍵向快取添加項目,GetProductsByCategoryID(categoryID)並且該方法為每個唯一的 categoryID 添加不同的快取項目。

當使快取失效時,我們需要刪除 ProductsCL 類別可能新增的所有項目。 這可以透過將 快取相依性與 AddCacheItem(key, value) 方法中新增至快取的每個項目相關聯來實現 。 一般來說,快取依賴項可以是快取中的另一個項目、檔案系統上的檔案或 Microsoft SQL Server 資料庫中的資料。 當依賴項變更或從快取中刪除時,與其關聯的快取項目將自動從快取中逐出。 對於本教學課程,我們希望在快取中建立一個附加項目,作為透過 ProductsCL 類別新增的所有項目的快取相依性。 這樣,只需刪除快取依賴項即可從快取中刪除所有這些項目。

讓我們更新 AddCacheItem(key, value) 方法,以便透過此方法新增至快取的每個項目都與單一快取相依性關聯:

Private Sub AddCacheItem(ByVal rawKey As String, ByVal value As Object)
    Dim DataCache As System.Web.Caching.Cache = HttpRuntime.Cache
    ' Make sure MasterCacheKeyArray[0] is in the cache - if not, add it
    If DataCache(MasterCacheKeyArray(0)) Is Nothing Then
        DataCache(MasterCacheKeyArray(0)) = DateTime.Now
    End If
    ' Add a CacheDependency
    Dim dependency As New Caching.CacheDependency(Nothing, MasterCacheKeyArray) _
        DataCache.Insert(GetCacheKey(rawKey), value, dependency, _
        DateTime.Now.AddSeconds(CacheDuration), _
        System.Web.Caching.Cache.NoSlidingExpiration)
End Sub

MasterCacheKeyArray 是一個包含單一值 ProductsCache 的字串陣列。 首先,將快取項目新增到快取中並分配當前日期和時間。 如果快取項目已存在,則更新它。 接下來,建立快取相依性。 CacheDependency 類別的建構函式有許多重載,但此處使用的建構函式需要兩個 String 陣列輸入。 第一個指定要用作依賴項的檔案集。 由於我們不想使用任何基於檔案的依賴項,因此第一個輸入參數使用 Nothing 值。 第二個輸入參數指定用作依賴項的快取鍵集。 在這裡我們指定我們的單一依賴項 MasterCacheKeyArray。 然後將 CacheDependency 傳遞到 Insert 方法中。

透過 的修改,使快取 AddCacheItem(key, value) 無效就像刪除依賴項一樣簡單。

<DataObjectMethodAttribute(DataObjectMethodType.Update, False)> _
Public Function UpdateProduct(ByVal productName As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal productID As Integer) _
    As Boolean
    Dim result As Boolean = API.UpdateProduct(productName, unitPrice, productID)
    ' Invalidate the cache
    InvalidateCache()
    Return result
End Function
Public Sub InvalidateCache()
    ' Remove the cache dependency
    HttpRuntime.Cache.Remove(MasterCacheKeyArray(0))
End Sub

步驟 5:從表示層呼叫快取層

快取層的類別和方法可用於使用我們在這些教學課程中研究過的技術來處理資料。 為了說明如何使用快取資料,請將變更儲存到 ProductsCL 類別中,然後開啟 Caching 資料夾中的 FromTheArchitecture.aspx 頁面並新增 GridView。 從 GridView 的智慧標記建立一個新的 ObjectDataSource。 在精靈的第一步中,您應該會看到 ProductsCL 類別作為下拉清單中的選項之一。

ProductsCL 類別包含在業務物件下拉清單中

圖 4ProductsCL 類別包含在業務物件下拉清單中 (按一下查看大圖)

選擇 ProductsCL 後,按下一步。 SELECT 標籤中的下拉清單有兩個項目 GetProducts() - GetProductsByCategoryID(categoryID),並且 UPDATE 標籤具有唯一的 UpdateProduct 重載項目。 從 SELECT 標籤中選擇 GetProducts() 方法,從 UPDATE 標籤中選擇 UpdateProducts 方法,然後按一下 Finish。

ProductsCL 類別的方法列在下拉清單中

圖 5ProductsCL 類別的方法列在下拉清單中 )按一下查看大圖)

完成精靈後,Visual Studio 將設定 ObjectDataSource 的 OldValuesParameterFormatString 屬性 original_{0} 並將適當的欄位新增至 GridView。 將 OldValuesParameterFormatString 屬性變更回其預設值 {0},並將 GridView 設定為支援分頁、排序和編輯。 由於 CL 使用的 UploadProducts 重載僅接受編輯的產品名稱和價格,因此限制 GridView 以便只有這些欄位是可編輯的。

在前面的教學中,我們定義了 GridView 以包含 ProductNameCategoryNameUnitPrice 欄位。 請隨意複製此格式和結構,在這種情況下,您的 GridView 和 ObjectDataSource 宣告性標記應類似於以下內容:

<asp:GridView ID="Products" runat="server" AutoGenerateColumns="False" 
    DataKeyNames="ProductID" DataSourceID="ProductsDataSource" 
    AllowPaging="True" AllowSorting="True">
    <Columns>
        <asp:CommandField ShowEditButton="True" />
        <asp:TemplateField HeaderText="Product" SortExpression="ProductName">
            <EditItemTemplate>
                <asp:TextBox ID="ProductName" runat="server" 
                    Text='<%# Bind("ProductName") %>' />
                <asp:RequiredFieldValidator ID="RequiredFieldValidator1"
                    ControlToValidate="ProductName" Display="Dynamic" 
                    ErrorMessage="You must provide a name for the product." 
                    SetFocusOnError="True"
                    runat="server">*</asp:RequiredFieldValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label2" runat="server" 
                    Text='<%# Bind("ProductName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:BoundField DataField="CategoryName" HeaderText="Category" 
            ReadOnly="True" SortExpression="CategoryName" />
        <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
            <EditItemTemplate>
                $<asp:TextBox ID="UnitPrice" runat="server" Columns="8" 
                    Text='<%# Bind("UnitPrice", "{0:N2}") %>'></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator1" runat="server" 
                    ControlToValidate="UnitPrice" Display="Dynamic" 
                    ErrorMessage="You must enter a valid currency value with 
                        no currency symbols. Also, the value must be greater than 
                        or equal to zero."
                    Operator="GreaterThanEqual" SetFocusOnError="True" 
                    Type="Currency" ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemStyle HorizontalAlign="Right" />
            <ItemTemplate>
                <asp:Label ID="Label1" runat="server" 
                    Text='<%# Bind("UnitPrice", "{0:c}") %>' />
            </ItemTemplate>
        </asp:TemplateField>
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ProductsDataSource" runat="server" 
    OldValuesParameterFormatString="{0}" SelectMethod="GetProducts" 
    TypeName="ProductsCL" UpdateMethod="UpdateProduct">
    <UpdateParameters>
        <asp:Parameter Name="productName" Type="String" />
        <asp:Parameter Name="unitPrice" Type="Decimal" />
        <asp:Parameter Name="productID" Type="Int32" />
    </UpdateParameters>
</asp:ObjectDataSource>

此時我們有一個使用快取層的頁面。 若要查看快取的運作情況,請在 ProductsCL 類別 GetProducts()UpdateProduct 方法中設定斷點。 在瀏覽器中存取該頁面,並在排序和分頁時單步執行程式碼,以便查看從快取中提取的資料。 然後更新一筆記錄,注意快取已失效,因此,當資料反彈到 GridView 時,會從 BLL 擷取該記錄。

注意

本文隨附的下載中提供的快取層並不完整。 它只包含一個類別,ProductsCL,它只包含少數方法。 而且,只有單一 ASP.NET 頁面使用了 CL (~/Caching/FromTheArchitecture.aspx),所有其他頁面仍然直接引用 BLL。 如果您打算在應用程式中使用 CL,則來自表示層的所有呼叫都應轉到 CL,這將要求 CL 的類別和方法覆寫表示層目前使用的 BLL 中的那些類別和方法。

摘要

雖然可以使用 ASP.NET 2.0 SqlDataSource 和 ObjectDataSource 控制項在表示層套用快取,但理想情況下,將快取職責委託給體系結構中的單獨層。 在本教學課程中,我們建立了一個位於表示層和業務邏輯層之間的快取層。 快取層需要提供與 BLL 中存在的相同的類別和方法集,並從表示層呼叫。

我們在本教學和前面的教學中探索的快取層範例展示了反應式載入。 透過反應式加載,僅當發出資料請求並且快取中缺少該資料時,資料才會加載到快取中。 資料還可以主動載入到快取中,這是一種在實際需要資料之前將資料載入到快取中的技術。 在下一個教學中,當我們了解如何在應用程式啟動時將靜態值儲存到快取中時,我們將看到主動載入的範例。

快樂程式!

關於作者

Scott Mitchell 是七本 ASP/ASP.NET 書籍的作者和 4GuysFromRolla.com 的創始人,自 1998 年以來一直致力於 Microsoft Web 技術。 史考特是一名獨立顧問、培訓師和作家。 他的最新著作是 Sams Teach Yourself ASP.NET 2.0 in 24 Hours。 您可以撥打 mitchell@4GuysFromRolla.com 聯絡他。或者透過他的部落格, http://ScottOnWriting.NET部落格可以在以下位置找到。

特別感謝

本教學系列得到了許多有用的審閱者的審閱。 本教學的首席審閱者是 Teresa Murphy。 有興趣查看我即將發表的 MSDN 文章嗎? 如果是這樣,請留言給我 mitchell@4GuysFromRolla.com