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






4.91/5 (109投票s)
DbfDotNet 是一个非常快速、紧凑、完全托管的独立数据库/实体框架,适用于 .Net Framework。
- 下载 DbfDotNet_version_1.0_Demo_Only - 35.4 KB (可执行文件)
- 下载 DbfDotNet_version_1.0_Source - 65.33 KB (包含演示源代码)

引言
本文介绍了一个独立的、完全托管的数据库/实体引擎,它实现了固定宽度记录表和 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);
}
我希望我能写 | 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 接口,如果你想帮助我,请自愿参加。
区别
使用代码
警告:此项目尚处于起步阶段,未经彻底测试。
你可以尝试一下,但请不要在实际生产环境中使用。
如果你想要速度,并且愿意报告或修复可能出现的任何问题:
- 创建一个 C# 项目
- 在你的项目中引用 DbfDotNet.dll
- 创建一个记录类
- 编写一些代码来处理记录
第 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。