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

用 C# 构建嵌入式数据库引擎

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (109投票s)

2009 年 6 月 1 日

CPOL

8分钟阅读

viewsIcon

358816

downloadIcon

10116

DbfDotNet 是一个非常快速、紧凑、完全托管的独立数据库/实体框架,适用于 .Net Framework。

引言

本文介绍了一个独立的、完全托管的数据库/实体引擎,它实现了固定宽度记录表和 BTree 索引。 

最新的源代码可在 CodePlex 上获得: http://dbfdotnet.codeplex.com/ 

欢迎任何想要为本项目做出贡献的人。 

为什么选择嵌入式数据库 

虽然我们大多数人会使用 SQL Server 来存储和检索数据集。
但在某些情况下,嵌入式数据库是明智的选择。

  • 当你无法使用 SQL Server 时
  • 当你希望占用空间尽可能小,并且无法负担 SQL Express 时
  • 当你想要处理或缓存 SQL 数据时
  • 当你需要编写高度过程化的数据处理例程时
  • 当你想要最大化速度时

特点

尽管体积小巧,DbfDotNet 仍提供了一些你可能会觉得有用的功能

  • 类型安全

在 DbfDotNet 中,你使用原生字段类型的类进行操作。所有数据转换的底层工作都自动完成。

  • 非常简单的实体框架

创建记录和访问其属性是你唯一需要做的。

  • 内存占用极小

上次我检查时,dbfDotNet 的 dll 文件只有 50KB。其他数据库通常在 1MB 到 10MB 之间。

我希望有人能做一些内存使用比较(我会在此处插入)。

  • 快速

DbfDotNet 的设计初衷就是为了速度。

DbfDotNet 不使用 PInvoke、线程锁,也不实现任何事务系统。
这三种技术都有性能成本,而它不必为此付费。

相反,它使用的是类型安全的记录(没有装箱/拆箱)和类型安全发出的代码。代码只为每个表生成一次。

因此,我相信它有潜力成为最快的嵌入式 .Net 数据库。

我希望有人能做一些速度比较(我会在此处插入)。

  • 运行时内存占用极小

当你使用内存中的 DataTable 或返回 DataSet 的 SQL 查询时,整个结果集都会保存在内存中。

DbfDotNet 与垃圾回收器协同工作。一旦你完成对一个实体的修改,垃圾回收器就会标记该记录缓冲区以保存到磁盘并从内存中释放。

为什么选择 Dbf

默认情况下,文件与 dBase 兼容,因此可以在 Excel 和许多其他程序中打开。

有人问我:为什么选择 Dbf?Dbf 是一个很老的格式。

答案有点长,但很简单。

正如我之前所说,DbfDotNet 的设计目标是尽可能快。

为了让数据库启动并引起一些兴趣,我需要两样东西:

  • 一个好产品
  • 一个好的用户群

凭经验我知道 DBF 格式会对你们中的一些人产生吸引力,原因如下:

  • 你可以轻松备份 DBF 文件(并保留索引文件)
  • 你可以使用 Excel 和许多其他工具检查 DBF 内容
  • DBF 是众所周知的,易于实现
  • 它可以扩展到现代类型(clipper 和 fox pro 已经做到了)

对我来说最重要的是,实现 .DBF 而不是我自己的自定义格式,对运行时速度没有影响。

与 ADO.Net、SQL、SqlLite、SharpSQL 等相比如何? 

我做了一些与另一个数据库(我不会透露名称)的速度测试。

结果相当令人鼓舞。 

 Dbf.Net  ADO.Net 
Opening DbfDotNetDatabase: 185 ms
Insert 1000 individuals: 39 ms
Read individuals sequentially: 5 ms
Read individual randomly: 3 ms
Modifying individuals: 21 ms
Create DateOfBirth index: 77 ms
     Michael Simmons 22/07/1909
     Mark Adams 21/09/1909
     Charles Edwards 28/09/1909
     ... total 1000 records
Enumerate Individuals by age: 36 ms
Closing DbfDotNetDatabase: 44 ms
Opening ADO.Net Database: 459 ms
Insert 1000 individuals: 80601 ms
Read individuals sequentially: 1655 ms
Read individual randomly: 1666 ms
Modifying individuals: 75574 ms
Create DateOfBirth index: 80 ms
     Michael Simmons 22/07/1909
     Mark Adams 21/09/1909
     Charles Edwards 28/09/1909
     ... total 1000 records
Enumerate Individuals by age: 29 ms
Closing ADO.Net Database: 0 ms

在此测试中,Dbf.Net 的运行速度几乎快了 400 倍。不过,这很不公平。Dbf.Net 没有事务,也不是 ACID 的。

让我们不要过于关注速度,而更多地关注代码差异: 

创建表 

创建表的方式大不相同。Dbf.Net 需要一个类型安全的记录才能创建表。 而在 ADO.Net 中,你提供一个字符串。 

 

 Dbf.Net ADO.Net 
DbfTable<dbfdotnetindividual> mIndividuals;

void CreateIndividualTable()
{
  mIndividuals = 
    new DbfTable<dbfdotnetindividual>(
      @"individuals.dbf", 
      Encoding.ASCII, 
      DbfDotNet.DbfVersion.dBaseIV);
}


class Individual
 : DbfDotNet.DbfRecord, IIndividual
 {
  [DbfDotNet.Column(Width = 20)]
  public string FIRSTNAME;
  [DbfDotNet.Column(Width = 20)]
  public string MIDDLENAME;
  [DbfDotNet.Column(Width = 20)]
  public string LASTNAME;
  public DateTime DOB;
  [DbfDotNet.Column(Width = 20)]
  public string STATE;
 }

Connection _cnn = null;


void ITestDatabase.CreateIndividualTable()
{
  _cnn = new System.Data.Connection(
"Data Source=adoNetTest.db");
  _cnn.Open();
  using (DbCommand cmd = _cnn.CreateCommand())
 {
   cmd.CommandText = "CREATE TABLE 
     INDIVIDUAL (ID int primary key, 
     FIRSTNAME VARCHAR(20), 
     MIDDLENAME VARCHAR(20), 
     LASTNAME VARCHAR(20), 
     DOB DATE, 
     STATE VARCHAR(20))";

    cmd.ExecuteNonQuery();
  }
}

向表中插入新条目: 

插入条目再次不同,在 ADO 中你必须构建一个命令字符串。在 DbfDotNet 中,你只需调用 NewRecord() 方法并设置字段。 Dbf.Net 会自动使用你提供的类来创建表。 调用 SaveChanges() 并非强制,但如果你希望控件立即刷新,则很有用。 

 Dbf.Net ADO.Net 
void InsertNewIndividual(
   int id, 
   string firstname,
   string middlename,
   string lastname,
   DateTime dob,
   string state)
{
  var indiv = mIndividuals.NewRecord();
  indiv.FIRSTNAME = firstname;
  indiv.MIDDLENAME = middlename;
  indiv.LASTNAME = lastname;
  indiv.DOB = dob;
  indiv.STATE = state;
  indiv.SaveChanges();
}

void InsertNewIndividual(
  int id, 
  string firstname, 
  string middlename, 
  string lastname,
  DateTime dob, 
  string state)
{
 using (DbCommand cmd =
   _cnn.CreateCommand())
 {
  cmd.CommandText = string.Format(
   "INSERT INTO INDIVIDUAL (ID,
    FIRSTNAME, MIDDLENAME, LASTNAME, 
    DOB, STATE) VALUES({0},
    '{1}', '{2}', '{3}', 
    '{4}', '{5}');",
   id, firstname, middlename,
   lastname,
   dob.ToString("yyyy-MM-dd HH:mm:ss"),
   state);
  cmd.ExecuteNonQuery();
 }
}

按记录 ID 获取单个记录  

获取单个记录也不同,在 ADO.Net 中你必须构建一个命令字符串。在 Dbf.Net 中,你调用一个方法。 同样,Dbf.Net 会自动使用你提供的类来创建表。 你是否看到了这里的模式? 

 Dbf.Net ADO.Net 
IIndividual GetIndividualById(int id)
{
  DbfDotNetIndividual result =
    mIndividuals.GetRecord(id);
    return result;
}
IIndividual GetIndividualById(int id)
{
 using (DbCommand cmd =
   _cnn.CreateCommand())
 {
  cmd.CommandText = string.Format(
    "SELECT * FROM INDIVIDUAL
     WHERE ID=" + id);
  var reader = cmd.ExecuteReader();
  try
  {
   if (reader.Read())
    return GetNewIndividual(reader);
   else return null;
  }
  finally
  {
   reader.Close();
  }
 }
}

Individual GetNewIndividual(
DbDataReader reader)
{
 var res = new Individual();
 res.ID = reader.GetInt32(0);
 res.FirstName = reader.GetString(1); 
 res.MiddleName = reader.GetString(2);
 res.LastName = reader.GetString(3);
 res.Dob = reader.GetDateTime(4);
 res.State = reader.GetString(5);
 return res;
}

 class Individual : IIndividual
 {
  public int ID { get; set; }
  public string FirstName { get; set; }
  public string MiddleName { get; set; }
  public string LastName { get; set; }
  public DateTime Dob { get; set; }
  public string State { get; set; }
 }

将已修改的个人保存回数据库。

在 Dbf.Net 中,你无需编写任何代码;如果你不想等待垃圾回收器收集你的个人数据,你可以调用 SaveChanges 。

 Dbf.Net  ADO.Net 
void SaveIndividual(
  Individual individual)
{
  individual.SaveChanges();
}


void SaveIndividual(
  IIndividual individual)
{
 using (DbCommand cmd =
   _cnn.CreateCommand())
 {
  cmd.CommandText = string.Format(
    "UPDATE INDIVIDUAL
 SET DOB='{1}' WHERE ID={0};",
 individual.ID,
 individual.Dob.ToString(
   "yyyy-MM-dd HH:mm:ss"));
  cmd.ExecuteNonQuery();
 }
}

创建索引 

在 ADO.Net 中,你必须构建一个命令字符串。在 Dbf.Net 中,你调用一个方法。 
尽管 AddField("DOB") 看起来不类型安全,但它在内部会发出代码,并且完全类型安全。 
 Dbf.Net  ADO.Net 
void CreateDobIndex()
{
  var sortOrder = 
    new DbfDotNet.SortOrder<Individual>(
    /*unique*/false);
    sortOrder.AddField("DOB");
    mDobIndex = mIndividuals.GetIndex(
    "DOB.NDX", sortOrder);
}

我希望我能写 sortOrder.AddField(DOB) ,但这行不通。有人对此有何想法吗?

void CreateDobIndex()
{
 using (DbCommand cmd =
   _cnn.CreateCommand())
 {
  cmd.CommandText =
   string.Format(
   "CREATE INDEX DOB_IDX ON 
    INDIVIDUAL (DOB)");
  cmd.ExecuteNonQuery();
 }
}

按年龄获取已排序的个人 

使用索引很简单,无需进行“SELECT”命令,只需使用 foreach 遍历索引即可。
 Dbf.Net  ADO.Net 
IEnumerable<Individual>
  IndividualsByAge()
{
  foreach (Individual indiv
    in mDobIndex)
  {
    yield return indiv;
  }
}
IEnumerable<Individual> 
  IndividualsByAge()
{
 using (DbCommand cmd =
   _cnn.CreateCommand())
 {
  cmd.CommandText = string.Format(
    "SELECT * FROM INDIVIDUAL
    ORDER BY DOB");
  var reader = cmd.ExecuteReader();
  try
  {
   while (reader.Read())
   {
    yield return 
      GetNewIndividual(reader);
   }
  }
  finally
  {
   reader.Close();
  }
 }
}

如你所见,使用 DbfDotNet 的代码通常要短得多。 

我试图避免提供字符串形式的命令。  

相反,我试图让它使用类型安全的成员,并总体上更加面向对象。 

高级接口 

有人问我如何与其他 SQL 数据库进行比较。

同样,DbfDotNet 不是一个 SQL 引擎。

它更像是一个对象持久化框架,例如 Microsoft Entity Framework 或 NHibernate。

区别在于,它不将对象操作转换为 SQL 请求,因为它直接与数据库层通信。

我很想编写一个合适的 Dbf 到 Linq 接口,如果你想帮助我,请自愿参加。 

区别 

使用代码 

警告:此项目尚处于起步阶段,未经彻底测试。 

你可以尝试一下,但请不要在实际生产环境中使用。 

如果你想要速度,并且愿意报告或修复可能出现的任何问题: 

  1. 创建一个 C# 项目
  2. 在你的项目中引用 DbfDotNet.dll
  3. 创建一个记录类
  4. 编写一些代码来处理记录

第 3 点和第 4 点将在下面详细介绍。

DbfRecord 类

DbfRecord 类代表你表中的一行。

你可以使用 column 属性来更改 DBF 特定的参数。

    class Individual : DbfDotNet.DbfRecord
    {
        [Column(Width = 20)]        public string FIRSTNAME;
        [Column(Width = 20)]        public string MIDDLENAME;
        [Column(Width = 20)]        public string LASTNAME;
        public DateTime DOB;
        [Column(Width = 20)]        public string STATE;
    }

系统会自动选择最适合你数据类型的 DbfField。

DbfTable 类

为了存储你的记录,你需要创建一个 Table。

 individuals = new DbfTable<Individual>(
      @"individuals.dbf", 
      Encoding.ASCII, 
      DbfVersion.dBaseIV);

请注意,这里使用的是类型安全的模板。表中的每个记录都是 individual (个人)。

记录操作

你可以使用 NewRecord 向表中添加新行。

    var newIndiv = individuals.NewRecord();

然后,你只需使用记录中的字段即可。

    newIndiv.LASTNAME = "GANAYE";

可选地,你可以调用 SaveChanges 来立即保存你的更改。
如果你不这样做,数据将在你的个人被垃圾回收时保存。

    newIndiv.SaveChanges();

索引支持

这仍然非常基础。首先,你需要定义你的排序顺序。

   var sortOrder = new SortOrder<Individual>(/* unique */ false); 
   sortOrder.AddField("LASTNAME");

然后,你可以获取你的索引。

   mIndex = individuals.GetIndex("lastname.ndx", sortOrder);

然后,你可以以类型安全的方式,从索引中检索任何个人记录。

   individual = mIndex.GetRecord(rowNo);

为了最大化速度,索引会发出自己的类型安全代码来

  • 从 DBF 记录中读取索引字段
  • 读取和写入索引条目
  • 比较索引条目

内部架构

DbfDotNet 的主类是 ClusteredFile

ClusteredFile 是对流的封装,提供分页和缓存支持。

ClusteredFile 是 DbfFile 和 NdxFile 的基类。当我编写它们时,它也将是 memo 文件的基类。

ClusteredFile 使用名为 QuickSerializer 的类将记录内容序列化为字节数组。

QuickSerializer 解析记录字段,并为每个字段生成一小段 IL 代码,以便于读取、保存和比较。

NdxFile 实现了一个 B+Tree 索引。

路线图

我的计划是保持这个库的体积非常小。我无意实现任何事务或多线程支持。

我将实现

  • 支持所有 DBF 字段类型
  • memo 字段(VARCHAR 类型)
  • 多个索引文件(*.mdx)
  • 完善的文档
  • LINQ(在一个单独的 dll 中)

如果你想帮助我完成这个项目,请联系我。

关注点

为了最大化速度,我强迫自己不使用任何线程同步锁定。

每组 Dbf + 索引必须从给定的线程调用。
换句话说,每个 dbf 文件及其索引只能由一个线程使用。

不过,我在垃圾回收器最终处理记录时遇到了一个问题,这是在垃圾回收器线程中完成的。我不想锁定资源,于是写了这段代码。

class Record
{
   private RecordHolder mHolder;

   ~Record()
   {
      try
      {
         ...
      }
      finally   
      {
         mHolder.RecordFinalized.Set();
      }
   }
}

每个记录都有一个 RecordHolder ,它存储一个 ReadBuffer 和可能的 WriteBuffer。

当记录被最终处理时,它会通知 RecordHolder 记录已被最终处理。这个指令不是阻塞的,它会引发一个可以在其他线程中使用的标志。

class ClusteredFile
{
   internal virtual protected Record InternalGetRecord(UInt32 recordNo)
   {
      RecordHolder holder = null;
      if (!mRecordsByRecordNo.TryGetValue(recordNo, out holder)) {...}
      
      record = holder.mRecordWeakRef.Target;
      if (record==null)
      {
         // the object is not accessible it has finalized a while ago or is being finalized 
         if (holder.RecordFinalized.WaitOne())
         {
            //Now it has finalized we will create a new record
            holder.RecordFinalized.Reset();
            holder.Record = OnCreateNewRecord(/*isnew*/false, recordNo);
         }
      }
      return holder.Record;
   }
}

然后,当表线程尝试获取正在被处理的记录时,我们使用方法: holder.RecordFinalized.WaitOne() 来确保最终处理已首先完成。大多数情况下,该方法不会阻塞你的 DBF 线程,因为记录早已被处理完毕。 

历史

2009 年 6 月 4 日:添加了示例和 ADO.Net 对比。
2009 年 6 月 1 日:发布了第一个 DbfDotNet (C#) 版本。 

2000 年 5 月 21 日:我编写了我的第一个数据库引擎,名为 tDbf,适用于 Delphi。 

© . All rights reserved.