65.9K
CodeProject 正在变化。 阅读更多。
Home

数据库本地缓存

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.33/5 (4投票s)

2007年12月14日

CPOL

8分钟阅读

viewsIcon

56698

downloadIcon

698

一个使用本地文件系统缓存数据库中存储的二进制对象的 C# 类。

引言

假设您正在开发一个用于管理健身中心的桌面应用程序。会员信息当然要存储在数据库中,因此您需要定义一个包含姓名、出生日期、电话等字段的“Members”表。但是,当您面临将会员照片与其余数据一起记录的要求时,您的喜悦将随之结束。然后,人类历史上第二个最重要的疑问(仅次于生命的意义)浮现在您的脑海中:是为了数据一致性而将照片存储在数据库中,还是为了性能而将其存储在本地文件系统中?

本文介绍了一个 C# 类,该类将使您能够兼顾两全其美。

背景

有时我们需要将一些大型二进制数据存储为更大的关系数据集的一部分。关于此类数据应该存储在哪里,这个争论由来已久。基本上,有两种选择,每种选择都有其独特的优点:一种是使用数据库,它提供事务完整性,并允许以一致的方式将所有数据联系在一起;另一种是使用磁盘文件,当必须快速检索数据时,这是无与伦比的,并且确实可以节省大量网络流量。

本文介绍了一个 C# 类,名为 **DatabaseFileCache**,旨在兼顾这两种方法的优点。基本思想是将权威的二进制数据存储在数据库中,然后将其缓存到本地文件系统中。

因此,应用程序第一次请求某个二进制对象时,会从数据库检索该对象并将其缓存到本地文件。下一次请求同一对象时,将返回缓存的文件,除非该对象自上次请求以来发生了更改。为了跟踪更改,使用了时间戳列;时间戳值作为文件名的一部分存储在本地缓存中。

工作原理

要使用该类,我们首先需要一个数据库表来存储二进制对象(“二进制对象”是指任何不透明的字节序列)。该表的默认名称为Objects(“默认名称”是该类使用的名称,除非您明确指定其他名称),它包含以下列:

  • 一个字符串(varchar 或类似类型)列,用于存储对象名称(默认名称为Name)。
  • 一个图像、varbinary 或类似类型列,用于存储对象的实际二进制数据(默认名称为Value)。
  • 一个时间戳列,用于跟踪对象更改(默认名称为timestamp)。

这里允许一些灵活性。值列可以是 ADO.NET 可以转换为字节数组的任何类型,时间戳列可以是任何类型,前提是当值列更改时它会自动更改(顺便说一句,时间戳数据类型就是为此目的而创建的)。表中可以存在额外的列,只要它们允许为空值、具有默认值,或者在创建新记录时由数据库引擎自动填充即可。

我们还需要本地文件系统中的一个目录来存储缓存的文件。默认情况下,该目录名为DatabaseCache,位于 DataDirectory 应用程序域属性指定的目录之下(是的,这与 SQL Server 2005 Express 连接字符串使用的 DataDirectory 是同一个;您可以使用 AppDomain.CurrentDomain.SetData 方法设置它)。如果未设置 DataDirectory,则假定 DatabaseCache 目录位于主应用程序可执行目录中(使用 Assembly.GetEntryAssembly().Location 获取此信息)。

该类提供的核心操作是根据名称检索二进制对象。这是此操作的伪代码算法:

Is there a file in the local cache with a matching name?
  No
    Retrieve the object from the database
    If no object with such name exists in database, return NULL
    Store the object in a file in the local cache
    Return the cached file
  Yes
    Obtain the current timestamp for the object in the database
    If no object with such name exists in database
      Delete the cached file
      Return NULL
    Obtain the timestamp of the cached file
    Do both timestamps match?
      Yes
        Return the cached file
      No
        Delete the cached file
        Retrieve the object from the database
        Store the object in a file in the local cache
        Return the cached file

如您所见,二进制对象仅在需要时才从数据库中检索;即,当对象是第一次被请求,并且自缓存以来发生过更改时;在所有其他情况下,仅从数据库检索时间戳(几个字节),并直接返回缓存的文件。

另请注意,伪缓存文件(存在于本地缓存但不在数据库中的对象)会被正确检测和删除。这些伪文件可能会出现,例如,当数据库被多个用户访问时,其中一个用户删除了数据库中的对象,而另一个用户已经缓存了该对象。

关于对象名称

要命名数据库中的二进制对象,可以使用任何字符串。在本地文件系统中存储这些对象时,文件名由对象名称组成,如下所示。首先,将文件名中无效的字符(如“*”或“/”)替换为序列%uuuu,其中uuuu是字符的十六进制 Unicode 表示形式;“%”字符本身和点也会被替换。接下来,将时间戳值转换为其十六进制表示形式,并作为扩展名附加到文件名中。

例如,假设您按以下方式命名您的对象:

Fast retransmit *really* increases speed in 20% over TCP/IP.pdf

然后,假设一个虚构的 4 字节时间戳,缓存文件名将如下所示:

Fast retransmit %002Areally%002A increases speed 
         in 20%0025 over TCP%002FIP%002Epdf.0045AB7F

这种编码的字符串可以很容易地转换回其原始的未编码版本。

使用代码

要使用此代码,您需要创建 DatabaseFileCache 类的实例。每个实例都与特定数据库中的给定表以及给定的本地缓存路径相关联(您可以在类构造函数中指定这些参数,或稍后通过类属性进行指定)。创建实例后,您可以使用其方法将对象存储到数据库中,并使用前面解释的缓存机制检索它们。

要实例化该类,您有三个构造函数:

  • 一个简单的构造函数,只接受一个参数:数据库的连接对象。所有其他参数(数据库表和列名以及本地缓存位置)都设置为默认值。
  • 一个中等复杂的构造函数,它接受连接对象、数据库表名和本地缓存位置作为参数。表列名设置为默认值。
  • 一个完整的构造函数,它接受连接对象、数据库表名、表列名和本地缓存位置作为参数。

您可以随时通过访问这些名称自明的公共属性来更改类的所有参数:ConnectionCachePathTableNameNameColumnValueColumnTimestampColumn。请注意,如果您为 CachePath 提供相对路径,它将与 DataDirectory 路径或应用程序可执行路径组合,如上所述。

还有一个额外的属性可能需要调整:CommandTimeout,它控制 SQL 命令执行的最长时间。默认为 30 秒;如果您处理非常大的对象或非常慢的网络,您可能需要将其设置为更大的值。

至于类公开的公共方法,它们是:

  • SaveObject:将指定的字节数组或指定文件的内容存储到数据库中,使用指定的名称或与文件名相同的名称(有三个方法重载)。如果数据库中已存在同名对象,其内容将被覆盖。此方法根本不访问本地缓存。
  • GetObject:将从数据库或本地缓存检索对象,遵循上述算法。返回缓存文件的路径,如果数据库中不存在指定名称的对象,则返回 null
  • GetCachedFile:如果存在,则返回指定对象名称的缓存文件路径,否则返回 null,根本不访问数据库。如果数据库变得无法访问,可以使用此方法作为“救生圈”,但前提是可以接受使用可能过时的数据。
  • DeleteObject:如果存在,则从数据库和本地缓存中删除指定名称的对象。
  • RenameObject:如果存在,则更改数据库和本地缓存中对象的名称。
  • ObjectExists:返回一个布尔值,指示数据库中是否存在指定名称的对象。
  • GetObjectNames:返回一个字符串数组,包含数据库中存储的所有对象的名称。
  • PurgeCache:从本地缓存中删除所有与数据库中对象不匹配的文件。如果数据库由多个用户访问,则可以在应用程序执行开始和/或结束时执行此方法一次可能很有用。

请注意,除了使用 PurgeCache 方法或代替它之外,您还可以随时手动删除部分或全部缓存文件。该类不维护有关缓存文件的任何状态信息,它仅在需要时搜索现有文件。

以下是一些使用该类的简单示例。例如,要创建一个实例,将数据库表命名为Photos,并将C:\PhotosCache作为缓存目录,您将执行以下操作:

SqlConnection connection=new SqlConnection(@"Data Source" + 
     @"=.\SQLEXPRESS;AttachDbFilename="|DataDirectory" + 
     @"|Data\MyDatabase.mdf";Integrated security=true;");
DatabaseFileCache cache=new DatabaseFileCache(connection, "Photos", @"C:\PhotosCache");

然后,将对象存储到数据库中:

cache.SaveObject(@"C:\Photos\DSCF0100.jpg", "Kaito's first birtday.jpg");
// Using an alternative method overload:
cache.SaveObject(new byte[] { 1,3,5,7,11 }, "Some primes");

然后检索数据:

string filePath=cache.GetObject("Kaito's first birtday.jpg")
MyForm.MyPictureBox.Load(filePath);

filePath=cache.GetObject("Some primes");
byte[] somePrimes=File.ReadAllBytes(filePath);

关注点

没有人是完美的(即使是我!)。DatabaseCache 类可以在许多方面进行改进/扩展。我在这里指出对我来说似乎最有趣的一些。

该类使用 SQL Server 作为数据库引擎,因此使用的所有连接、命令和参数对象都属于 System.Data.SqlClient 命名空间。将其适配到另一个提供程序,或者更进一步,使用 ADO.NET 工厂类以单一代码库支持任何提供程序,应该不难。

二进制数据以原始方式存储和检索到数据库:通过直接使用单个命令执行中的 byte[] 类型参数。这对于相对较小的对象来说效果很好,但对于非常大的对象,使用某种方法以小块发送和检索数据会更方便。

最后说明

这是我第一次向 Code Project 提交,所以请不要对我太严厉。

历史

  • 2007 年 12 月 14 日:初始版本。
© . All rights reserved.