Кэширование данных в архитектуре (VB)
В предыдущем руководстве мы узнали, как применить кэширование на уровне презентации. В этом руководстве мы узнаем, как использовать преимущества многоуровневой архитектуры для кэширования данных на уровне бизнес-логики. Для этого мы расширяем архитектуру, чтобы включить слой кэширования.
Введение
Как мы видели в предыдущем руководстве, кэширование данных ObjectDataSource так же просто, как задание нескольких свойств. К сожалению, ObjectDataSource применяет кэширование на уровне презентации, что тесно сцепляет политики кэширования со страницей ASP.NET. Одной из причин создания многоуровневой архитектуры является разорвание таких связей. Например, уровень бизнес-логики отделяет бизнес-логику от страниц ASP.NET, а уровень доступа к данным — сведения о доступе к данным. Это разделение бизнес-логики и сведений о доступе к данным предпочтительнее, отчасти потому, что это делает систему более читаемой, более поддерживаемой и более гибкой для изменений. Это также позволяет получить знания о предметной области и разделение труда, чтобы разработчик, работающий на уровне представления, не должен быть знаком с данными базы данных, чтобы выполнить свою работу. Отсоединение политики кэширования от уровня представления обеспечивает аналогичные преимущества.
В этом руководстве мы дополним нашу архитектуру, чтобы включить слой кэширования (или кратко cl), который использует нашу политику кэширования. Уровень кэширования будет включать класс, предоставляющий ProductsCL
доступ к сведениям о продукте с помощью таких методов, как GetProducts()
, GetProductsByCategoryID(categoryID)
и т. д., который при вызове сначала попытается получить данные из кэша. Если кэш пуст, эти методы вызывают соответствующий ProductsBLL
метод в BLL, который, в свою очередь, получает данные из DAL. Методы ProductsCL
кэшируют данные, полученные из BLL, прежде чем возвращать их.
Как показано на рисунке 1, cl находится между уровнями презентации и бизнес-логики.
Рис. 1. Слой кэширования (CL) — это еще один слой в нашей архитектуре
Шаг 1. Создание классов слоев кэширования
В этом руководстве мы создадим очень простую cl с одним классом ProductsCL
, который содержит только несколько методов. Создание полного уровня кэширования для всего приложения потребует создания CategoriesCL
классов , EmployeesCL
и SuppliersCL
и предоставления метода в этих классах уровня кэширования для каждого метода доступа к данным или изменения в BLL. Как и в случае с BLL и DAL, слой кэширования в идеале должен быть реализован как отдельный проект библиотеки классов; однако мы реализуем его как класс в папке App_Code
.
Чтобы более четко отделить классы CL от классов DAL и BLL, создадим в папке новую вложенную папку App_Code
. Щелкните правой кнопкой мыши папку App_Code
в Обозреватель решений, выберите Создать папку и назовите новую папку CL
. После создания этой папки добавьте в нее новый класс с именем ProductsCL.vb
.
Рис. 2. Добавление новой папки с именем CL
и класса с именем ProductsCL.vb
Класс ProductsCL
должен содержать тот же набор методов доступа к данным и их изменения, что и в соответствующем классе уровня бизнес-логики (ProductsBLL
). Вместо того, чтобы создавать все эти методы, давайте просто создадим пару здесь, чтобы получить представление о шаблонах, используемых CL. В частности, мы добавим методы и GetProductsByCategoryID(categoryID)
на GetProducts()
шаге 3 и перегрузку UpdateProduct
на шаге 4. Вы можете добавить оставшиеся ProductsCL
методы и CategoriesCL
классы , EmployeesCL
и SuppliersCL
в свободное время.
Шаг 2. Чтение и запись в кэш данных
Функция кэширования ObjectDataSource, рассмотренная в предыдущем руководстве, внутренне использует кэш данных ASP.NET для хранения данных, полученных из BLL. Доступ к кэшу данных также можно получить программным способом из ASP.NET классов кода программной части страниц или из классов в архитектуре веб-приложения. Для чтения и записи в кэш данных из класса кода программной части страницы 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
класса s Insert
имеет ряд перегрузок. Cache("key") = value
и Cache.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
Здесь Тип — это тип данных, хранящихся в кэше Northwind.ProductsDataTable
, например , а key — это ключ, который уникальным образом идентифицирует элемент кэша. Если элемент с указанным ключом отсутствует в кэше, экземпляр будет Nothing
иметь значение , а данные будут извлечены из соответствующего метода BLL и добавлены в кэш. К моменту Return instance
достижения экземпляр содержит ссылку на данные из кэша или из BLL.
Не забудьте использовать приведенный выше шаблон при доступе к данным из кэша. Следующий шаблон, который, на первый взгляд, выглядит эквивалентно, содержит незначительное различие, которое вводит состояние гонки. Условия гонки трудно отлаживать, так как они проявляются спорадически и трудно воспроизвести.
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
из класса
В этом руководстве мы реализуем два метода для возврата сведений о продукте ProductsCL
из класса: GetProducts()
и GetProductsByCategoryID(categoryID)
. Как и в случае с классом ProductsBL
уровня бизнес-логики, GetProducts()
метод в CL возвращает сведения обо всех продуктах в виде 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
Сначала обратите внимание на DataObject
атрибуты и DataObjectMethodAttribute
, применяемые к классу и методам. Эти атрибуты предоставляют мастеру ObjectDataSource сведения, указывающие, какие классы и методы должны отображаться в шагах мастера. Так как доступ к классам и методам CL будет осуществляться из ObjectDataSource на уровне представления, я добавил эти атрибуты для улучшения работы во время разработки. Более подробное описание этих атрибутов и их эффектов см. в руководстве По созданию уровня бизнес-логики .
В методах 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-. Объект MasterCacheKeyArray
, содержащий строку ProductsCache, также используется методом AddCacheItem(key, value)
, как мы увидим на мгновение.
Из класса кода программной части ASP.NET страницы доступ к кэшу данных можно получить с помощью Page
свойства класса s Cache
и обеспечивает синтаксис, например Cache("key") = value
, как описано в шаге 2. Из класса в архитектуре доступ к кэшу данных можно получить с помощью HttpRuntime.Cache
или HttpContext.Current.Cache
. Запись в блоге Питера ДжонсонаHttpRuntime.Cache и HttpContext.Current.Cache отмечает небольшое преимущество производительности при использовании HttpRuntime
вместо HttpContext.Current
. Следовательно, ProductsCL
использует HttpRuntime
.
Примечание
Если архитектура реализована с помощью проектов библиотеки классов, необходимо добавить ссылку на сборку System.Web
, чтобы использовать HttpRuntime
классы и HttpContext
.
Если элемент не найден в кэше 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.
Шаг 4. Недопустимость кэша при изменении данных с помощью архитектуры
Наряду с методами извлечения данных уровень кэширования должен предоставлять те же методы, что и BLL для вставки, обновления и удаления данных. Методы изменения данных CL не изменяют кэшированные данные, а вызывают соответствующий метод изменения данных BLL, а затем делают кэш недействительным. Как мы видели в предыдущем руководстве, это то же поведение, что и ObjectDataSource, когда включены функции кэширования и вызываются методы Insert
, Update
или Delete
.
Следующая 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
классе , а затем откройте FromTheArchitecture.aspx
страницу в папке Caching
и добавьте GridView. Из смарт-тега GridView создайте объект ObjectDataSource. На первом шаге мастера класс должен отображаться ProductsCL
как один из вариантов из раскрывающегося списка.
Рис. 4. Класс ProductsCL
включен в список бизнес-объектов Drop-Down (щелкните для просмотра полноразмерного изображения)
После выбора ProductsCL
нажмите кнопку Далее. В раскрывающемся списке на вкладке SELECT есть два элемента: GetProducts()
и GetProductsByCategoryID(categoryID)
на вкладке UPDATE есть единственная UpdateProduct
перегрузка. Выберите метод на GetProducts()
вкладке SELECT и UpdateProducts
метод на вкладке UPDATE и нажмите кнопку Готово.
Рис. 5. ProductsCL
Методы класса перечислены в Drop-Down Списки (Щелкните для просмотра полноразмерного изображения)
После завершения работы мастера Visual Studio установит свойству original_{0}
ObjectDataSource OldValuesParameterFormatString
значение и добавит соответствующие поля в GridView. Измените OldValuesParameterFormatString
свойство на его значение {0}
по умолчанию и настройте GridView для поддержки разбиения по страницам, сортировки и редактирования. Так как перегрузка UploadProducts
, используемая cl, принимает только имя и цену измененного продукта, ограничьте GridView, чтобы только эти поля были редактируемыми.
В предыдущем руководстве мы определили GridView, чтобы включить поля для ProductName
полей , CategoryName
и UnitPrice
. Вы можете реплицировать это форматирование и структуру. В этом случае декларативная разметка 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
методах . Перейдите на страницу в браузере и выполните пошаговое выполнение кода при сортировке и разбиении по страницам, чтобы просмотреть данные, полученные из кэша. Затем обновите запись и обратите внимание, что кэш становится недействительным и, следовательно, извлекается из BLL при отскоке данных в GridView.
Примечание
Уровень кэширования, предоставленный в сопроводительном файле этой статьи, не является полным. Он содержит только один класс , ProductsCL
который имеет только несколько методов. Кроме того, только на одной странице ASP.NET используется cl (~/Caching/FromTheArchitecture.aspx
), все остальные по-прежнему ссылались на BLL напрямую. Если вы планируете использовать cl в приложении, все вызовы из уровня презентации должны переходить в cl, что потребует, чтобы классы и методы CL охватывали эти классы и методы в BLL, которые в настоящее время используются уровнем представления.
Сводка
Хотя кэширование можно применять на уровне представления с помощью элементов управления SqlDataSource и ObjectDataSource ASP.NET 2.0, в идеале обязанности по кэшированию будут делегированы отдельному уровню в архитектуре. В этом руководстве мы создали слой кэширования, расположенный между уровнем представления и уровнем бизнес-логики. Уровень кэширования должен предоставить тот же набор классов и методов, которые существуют в BLL и вызываются из уровня представления.
В примерах уровня кэширования, которые мы рассмотрели в этом и предыдущих руководствах, показана реактивная загрузка. При реактивной загрузке данные загружаются в кэш только в том случае, если выполняется запрос данных и эти данные отсутствуют в кэше. Данные также можно заранее загрузить в кэш, что позволяет загружать данные в кэш до того, как они действительно понадобятся. В следующем руководстве мы рассмотрим пример упреждающей загрузки, когда рассмотрим, как сохранить статические значения в кэше при запуске приложения.
Счастливого программирования!
Об авторе
Скотт Митчелл( Scott Mitchell), автор семи книг ASP/ASP.NET и основатель 4GuysFromRolla.com, работает с веб-технологиями Майкрософт с 1998 года. Скотт работает независимым консультантом, тренером и писателем. Его последняя книга Sams Teach Yourself ASP.NET 2.0 в 24 часах. Он может быть доступен в mitchell@4GuysFromRolla.com. или через его блог, который можно найти по адресу http://ScottOnWriting.NET.
Особая благодарность
Эта серия учебников была рассмотрена многими полезными рецензентами. Ведущим рецензентом этого руководства была Тетера Мерфи. Хотите просмотреть предстоящие статьи MSDN? Если да, опустите мне строку на mitchell@4GuysFromRolla.com.