小型单文件NoSQL数据库SharpFileDB初步实现
我不是数据库方面的专家,不过还是想做一个小型的数据库,算是一种通过mission impossible进行学习锻炼的方式。我知道这是自不量力,不过还是希望各路大神批评的时候不要人身攻击,谢谢。
SharpFileDB
最近所做的多文件数据库是受(C#实现文件数据库)的启发。后来又发现了(LiteDB),看到了单文件数据库和分页、索引、查询语句等的实现方式,大受启发。不过我仍旧认为LiteDB使用起来有些不顺畅,它的代码组织也不敢完全苟同。所以,我重新设计了一个小型的单文件数据库SharpFileDB:
无需配置服务器。
无需SQL。
100%纯C#开发的一个不到50KB的DLL。
支持事务ACID。
写入失败后可恢复(日志模式)。
可存储任意继承了Table且具有[Serializable]特性的类型(相当于关系数据库的Table)。类型数目不限。
可存储System.Drawing.Image等大型对象。
单文件存储,只要你的硬盘空间够大,理论上能支持的最大长度为long.MaxValue = 9223372036854775807 = 0x7FFFFFFFFFFFFFFF = 8589934591GB = 8388607TB = 8191PB = 7EB的大文件。
每个类型都可以建立多个索引,索引数目不限。只需在属性上加[TableIndex]特性即可实现。
支持通过Lambda表达式进行查询。
开源免费,2300行代码,1000行注释。
附带Demo、可视化的监视工具、可视化的数据库设计器,便于学习、调试和应用。
使用场景
假设已经做好了这样一个单文件数据库,我期望的使用方式是这样的:
string fullname = Path.Combine(Environment.CurrentDirectory, "test.db");
using (FileDBContext db = new FileDBContext(fullname))
{
Cat cat = new Cat();
string name = "kitty " + random.Next();
cat.KittyName = name;
cat.Price = random.Next(, ); db.Insert(cat); System.Linq.Expressions.Expression<Func<Cat, bool>> pre = null; pre = (x =>
(x.KittyName == "kitty" || (x.KittyName == name && x.Id.ToString() != string.Empty))
|| (x.KittyName.Contains("kitty") && x.Price > )
); IEnumerable<Cat> cats = db.Find<Cat>(pre); cats = db.FindAll<Cat>(); cat.KittyName = "小白 " + random.Next();
db.Update(cat); db.Delete(cat);
}
就像关系型数据库一样,我们可以创建各种Table(例如这里的Cat)。然后直接使用Insert(Table record);插入一条记录。创建自定义Table只需继承Table实现自己的class即可。
/// <summary>
/// 继承此类型以实现您需要的Table。
/// </summary>
[Serializable]
public abstract class Table : ISerializable
{ /// <summary>
/// 用以区分每个Table的每条记录。
/// This Id is used for diffrentiate instances of 'table's.
/// </summary>
[TableIndex]// 标记为索引,这是每个表都有的主键。
public ObjectId Id { get; internal set; } /// <summary>
/// 创建一个文件对象,在用<code>FileDBContext.Insert();</code>将此对象保存到数据库之前,此对象的Id为null。
/// </summary>
public Table() { } /// <summary>
/// 显示此条记录的Id。
/// </summary>
/// <returns></returns>
public override string ToString()
{
return string.Format("Id: {0}", this.Id);
} /// <summary>
/// 使用的字符越少,序列化时占用的字节就越少。一个字符都不用最好。
/// <para>Using less chars means less bytes after serialization. And "" is allowed.</para>
/// </summary>
const string strId = ""; #region ISerializable 成员 /// <summary>
/// This method will be invoked automatically when IFormatter.Serialize() is called.
/// <para>You must use <code>base(info, context);</code> in the derived class to feed <see cref="Table"/>'s fields and properties.</para>
/// <para>当使用IFormatter.Serialize()时会自动调用此方法。</para>
/// <para>继承此类型时,必须在子类型中用<code>base(info, context);</code>来填充<see cref="Table"/>自身的数据。</para>
/// </summary>
/// <param name="info"></param>
/// <param name="context"></param>
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
byte[] value = this.Id.Value;//byte[]比this.Id.ToString()占用的字节少2个字节。
info.AddValue(strId, value);
} #endregion /// <summary>
/// This method will be invoked automatically when IFormatter.Serialize() is called.
/// <para>You must use <code>: base(info, context)</code> in the derived class to feed <see cref="Table"/>'s fields and properties.</para>
/// <para>当使用IFormatter.Serialize()时会自动调用此方法。</para>
/// <para>继承此类型时,必须在子类型中用<code>: base(info, context)</code>来填充<see cref="Table"/>自身的数据。</para>
/// </summary>
/// <param name="info"></param>
/// <param name="context"></param>
protected Table(SerializationInfo info, StreamingContext context)
{
byte[] value = (byte[])info.GetValue(strId, typeof(byte[]));
this.Id = new ObjectId(value);
} }
这里的Cat定义如下:
[Serializable]
public class Cat : Table
{
/// <summary>
/// 显示此对象的信息,便于调试。
/// </summary>
/// <returns></returns>
public override string ToString()
{
return string.Format("{0}: ¥{1}", KittyName, Price);
} public string KittyName { get; set; } public int Price { get; set; } public Cat() { } const string strKittyName = "N";
const string strPrice = "P"; public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
{
base.GetObjectData(info, context); info.AddValue(strKittyName, this.KittyName);
info.AddValue(strPrice, this.Price);
} protected Cat(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
: base(info, context)
{
this.KittyName = info.GetString(strKittyName);
this.Price = info.GetInt32(strPrice);
} }
后面我提供了一个可视化的数据库设计器,你可以像在SQL Server Management里那样设计好你需要的表,即可一键生成相应的数据库项目源码。
从何开始
用C#做一个小型单文件数据库,需要用到.NET Framework提供的这几个类型。
FileStream
文件流用于操作数据库文件。FileStream支持随机读写,并且FileStream.Length属性是long型的,就是说数据库文件最大可以有long.MaxValue个字节,这是超级大的。
使用FileStream的方式是这样的:
var fileStream = new FileStream(fullname, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
这句代码指明:
fullname:打开绝对路径为fullname的文件。
FileMode.Open:如果文件不存在,抛出异常。
FileAccess.ReadWrite:fileStream对象具有读和写文件的权限。
FileShare.Read:其它进程只能读此文件,不能写。我们可以用其它进程来实现容灾备份之类的操作。
BinaryFormatter
读写数据库文件实际上就是反序列化和序列化对象的过程。我在这里详细分析了为什么使用BinaryFormatter。
联合使用FileStream和BinaryFormatter就可以实现操作数据库文件的最基础的功能。
/// <summary>
/// 使用FileStream和BinaryFormatter做单文件数据库的核心工作流。
/// </summary>
/// <param name="fullname"></param>
public static void TypicalScene(string fullname)
{
// 初始化。
BinaryFormatter formatter = new BinaryFormatter(); // 打开数据库文件。
FileStream fs = new FileStream(fullname, FileMode.Open, FileAccess.ReadWrite, FileShare.Read); // 把对象写入数据库。
long position = ;// 指定位置。
fs.Seek(position, SeekOrigin.Begin);
Object obj = new Object();// 此处可以是任意具有[Serializable]特性的类型。
formatter.Serialize(fs, obj);// 把对象序列化并写入文件。 fs.Flush(); // 从数据库文件读取对象。
fs.Seek(position, SeekOrigin.Begin);// 指定位置。
Object deserialized = formatter.Deserialize(fs);// 从文件得到反序列化的对象。 // 关闭文件流,退出数据库。
fs.Close();
fs.Dispose();
}
简单来说,这就是整个单文件数据库最基本的工作过程。后续的所有设计,目的都在于得到应指定的位置和应读写的对象类型了。能够在合适的位置写入合适的内容,能够通过索引实现快速定位和获取/删除指定的内容,这就是实现单文件数据库要做的第一步。能够实现事务和恢复机制,就是第二步。
MemoryStream
使用MemoryStream是为了先把对象转换成byte[],这样就可以计算其序列化后的长度,然后才能为其安排存储到数据库文件的什么地方。
/// <summary>
/// 把Table的一条记录转换为字节数组。这个字节数组应该保存到Data页。
/// </summary>
/// <param name="table"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] ToBytes(this Table table)
{
byte[] result;
using (MemoryStream ms = new MemoryStream())
{
Consts.formatter.Serialize(ms, table);
if (ms.Length > (long)int.MaxValue)// RULE: 一条记录序列化后最长不能超过int.MaxValue个字节。
{ throw new Exception(string.Format("Toooo long is the [{0}]", table)); }
result = new byte[ms.Length];
ms.Seek(, SeekOrigin.Begin);
ms.Read(result, , result.Length);
} return result;
}
准备知识
全局唯一编号
写入数据库的每一条记录,都应该有一个全局唯一的编号。(C#实现文件数据库)和(LiteDB)都有一个ObjectId类型,两者也十分相似,且存储它需要的长度也小于.NET Framework自带的Guid,所以就用ObjectId做全局唯一的编号了。
/// <summary>
/// 用于生成唯一的<see cref="Table"/>编号。
/// </summary>
[Serializable]
public sealed class ObjectId : ISerializable, IComparable<ObjectId>, IComparable
{
private string _string; private ObjectId()
{
} internal ObjectId(string value)
: this(DecodeHex(value))
{
} internal ObjectId(byte[] value)
{
Value = value;
} internal static ObjectId Empty
{
get { return new ObjectId(""); }
} internal byte[] Value { get; private set; } /// <summary>
/// 获取一个新的<see cref="ObjectId"/>。
/// </summary>
/// <returns></returns>
public static ObjectId NewId()
{
return new ObjectId { Value = ObjectIdGenerator.Generate() };
} internal static bool TryParse(string value, out ObjectId objectId)
{
objectId = Empty;
if (value == null || value.Length != )
{
return false;
} try
{
objectId = new ObjectId(value);
return true;
}
catch (FormatException)
{
return false;
}
} static byte[] DecodeHex(string value)
{
if (string.IsNullOrEmpty(value))
throw new ArgumentNullException("value"); var chars = value.ToCharArray();
var numberChars = chars.Length;
var bytes = new byte[numberChars / ]; for (var i = ; i < numberChars; i += )
{
bytes[i / ] = Convert.ToByte(new string(chars, i, ), );
} return bytes;
} /// <summary>
///
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
return Value != null ? ToString().GetHashCode() : ;
} /// <summary>
/// 显示此对象的信息,便于调试。
/// </summary>
/// <returns></returns>
public override string ToString()
{
if (_string == null && Value != null)
{
_string = BitConverter.ToString(Value)
.Replace("-", string.Empty)
.ToLowerInvariant();
} return _string;
} /// <summary>
///
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
var other = obj as ObjectId;
return Equals(other);
} /// <summary>
///
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool Equals(ObjectId other)
{
return other != null && ToString() == other.ToString();
} //public static implicit operator string(ObjectId objectId)
//{
// return objectId == null ? null : objectId.ToString();
//} //public static implicit operator ObjectId(string value)
//{
// return new ObjectId(value);
//} /// <summary>
///
/// </summary>
/// <param name="left"></param>
/// <param name="right"></param>
/// <returns></returns>
public static bool operator ==(ObjectId left, ObjectId right)
{
if (ReferenceEquals(left, right))
{
return true;
} if (((object)left == null) || ((object)right == null))
{
return false;
} return left.Equals(right);
} /// <summary>
///
/// </summary>
/// <param name="left"></param>
/// <param name="right"></param>
/// <returns></returns>
public static bool operator !=(ObjectId left, ObjectId right)
{
return !(left == right);
} #region ISerializable 成员 const string strValue = "";
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
string value = this.ToString();
info.AddValue(strValue, value);
} #endregion private ObjectId(SerializationInfo info, StreamingContext context)
{
string value = info.GetString(strValue);
this.Value = DecodeHex(value);
} #region IComparable<ObjectId> 成员 /// <summary>
/// 根据<see cref="ObjectId.ToString()"/>的值比较两个对象。
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public int CompareTo(ObjectId other)
{
if (other == null) { return ; } string thisStr = this.ToString();
string otherStr = other.ToString();
int result = thisStr.CompareTo(otherStr); return result;
} #endregion #region IComparable 成员 /// <summary>
/// 根据<see cref="ObjectId.ToString()"/>的值比较两个对象。
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public int CompareTo(object obj)
{
ObjectId other = obj as ObjectId;
return CompareTo(other);
} #endregion
} internal static class ObjectIdGenerator
{
private static readonly DateTime Epoch =
new DateTime(, , , , , , DateTimeKind.Utc);
private static readonly object _innerLock = new object();
private static int _counter;
private static readonly byte[] _machineHash = GenerateHostHash();
private static readonly byte[] _processId =
BitConverter.GetBytes(GenerateProcessId()); internal static byte[] Generate()
{
var oid = new byte[];
var copyidx = ; Array.Copy(BitConverter.GetBytes(GenerateTime()), , oid, copyidx, );
copyidx += ; Array.Copy(_machineHash, , oid, copyidx, );
copyidx += ; Array.Copy(_processId, , oid, copyidx, );
copyidx += ; Array.Copy(BitConverter.GetBytes(GenerateCounter()), , oid, copyidx, ); return oid;
} private static int GenerateTime()
{
var now = DateTime.UtcNow;
var nowtime = new DateTime(Epoch.Year, Epoch.Month, Epoch.Day,
now.Hour, now.Minute, now.Second, now.Millisecond);
var diff = nowtime - Epoch;
return Convert.ToInt32(Math.Floor(diff.TotalMilliseconds));
} private static byte[] GenerateHostHash()
{
using (var md5 = MD5.Create())
{
var host = Dns.GetHostName();
return md5.ComputeHash(Encoding.Default.GetBytes(host));
}
} private static int GenerateProcessId()
{
var process = Process.GetCurrentProcess();
return process.Id;
} private static int GenerateCounter()
{
lock (_innerLock)
{
return _counter++;
}
}
}
ObjectId
使用时只需通过调用ObjectId.NewId();即可获取一个新的编号。
分页机制
磁盘I/O操作每次都是以4KB个字节为单位进行的。所以把单文件数据库划分为一个个长度为4KB的页就很有必要。这一点稍微增加了数据库设计图的复杂程度。由于磁盘I/O所需时间最长,所以对此进行优化是值得的。
你可以随意新建一个TXT文件,在里面写几个字符,保存一下,会看到即使是大小只有1个字节内容的TXT文件,其占用空间也是4KB。
而且所有文件的"占用空间"都是4KB的整数倍。
索引机制
我从LiteDB的文档看到,它用Skip List实现了索引机制,能够快速定位读写一个对象。Skip List是以空间换时间的方式,用扩展了的单链表达到了红黑树的效率,而其代码比红黑树简单得多。要研究、实现红黑树会花费更多时间,所以我效仿LiteDB用Skip List做索引。
关于Skip List大家可以参考这里,有Skip List的实现代码。(还有很多数据结构和算法的C#实现,堪称宝贵)还有这里(http://www.cnblogs.com/xuqiang/archive/2011/05/22/2053516.html)的介绍也很不错。
Skip List的结构如下图所示。
你只需知道Skip List在外部看起来就像一个Dictionary<TKey, TValue>,它是通过Add(TKey key, TValue value);来增加元素的。每个Skip List Node都含有一个key和一个value,而且,同一列上的结点的key和value值都相同。例如,上图的key值为50的三个Skip List Node,其key当然都是50,而其value也必须是相同的。
关于Skip List的详细介绍可参考*。
查询语句
创建数据库、创建表、索引和删除表的语句都已经不需要了。
Lambda表达式可以用作查询语句。再次感谢LiteDB,给了我很多启发。
不利用索引的懒惰方案
解析Lambda表达式的工作量超出我的预期,暂时先用一个懒惰的方案顶替之。LiteDB提供的解析方式也有很大局限,我还要考虑一下如何做Lambda表达式的解析。
/// <summary>
/// 查找数据库内的某些记录。
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="predicate">符合此条件的记录会被取出。</param>
/// <returns></returns>
public IEnumerable<T> Find<T>(Expression<Func<T, bool>> predicate) where T : Table, new()
{
if (predicate == null) { throw new ArgumentNullException("predicate"); } // 这是没有利用索引的版本。
Func<T, bool> func = predicate.Compile();
foreach (T item in this.FindAll<T>())
{
if(func(item))
{
yield return item;
}
} // TODO: 这是利用索引的版本,尚未实现。
//List<T> result = new List<T>(); //var body = predicate.Body as LambdaExpression;
//this.Find(result, body); //return result;
}
/// <summary>
/// 查找数据库内所有指定类型的记录。
/// </summary>
/// <typeparam name="T">要查找的类型。</typeparam>
/// <returns></returns>
public IEnumerable<T> FindAll<T>() where T:Table, new()
{
Type type = typeof(T);
if (this.tableBlockDict.ContainsKey(type))
{
TableBlock tableBlock = this.tableBlockDict[type];
IndexBlock firstIndex = tableBlock.IndexBlockHead.NextObj;// 第一个索引应该是Table.Id的索引。
FileStream fs = this.fileStream; SkipListNodeBlock current = firstIndex.SkipListHeadNodes[]; //currentHeadNode; while (current.RightPos != firstIndex.SkipListTailNodePos)
{
current.TryLoadProperties(fs, SkipListNodeBlockLoadOptions.RightObj);
current.RightObj.TryLoadProperties(fs, SkipListNodeBlockLoadOptions.RightObj | SkipListNodeBlockLoadOptions.Value);
T item = current.RightObj.Value.GetObject<T>(this); yield return item; current = current.RightObj;
}
}
}
Lambda表达式
在MSDN上有关于Lambda表达式的介绍。
System.Object
System.Linq.Expressions.Expression
System.Linq.Expressions.BinaryExpression
System.Linq.Expressions.BlockExpression
System.Linq.Expressions.ConditionalExpression
System.Linq.Expressions.ConstantExpression
System.Linq.Expressions.DebugInfoExpression
System.Linq.Expressions.DefaultExpression
System.Linq.Expressions.DynamicExpression
System.Linq.Expressions.GotoExpression
System.Linq.Expressions.IndexExpression
System.Linq.Expressions.InvocationExpression
System.Linq.Expressions.LabelExpression
System.Linq.Expressions.LambdaExpression
System.Linq.Expressions.ListInitExpression
System.Linq.Expressions.LoopExpression
System.Linq.Expressions.MemberExpression
System.Linq.Expressions.MemberInitExpression
System.Linq.Expressions.MethodCallExpression
System.Linq.Expressions.NewArrayExpression
System.Linq.Expressions.NewExpression
System.Linq.Expressions.ParameterExpression
System.Linq.Expressions.RuntimeVariablesExpression
System.Linq.Expressions.SwitchExpression
System.Linq.Expressions.TryExpression
System.Linq.Expressions.TypeBinaryExpression
System.Linq.Expressions.UnaryExpression
这个列表放在这里是为了方便了解lambda表达式都有哪些类型的结点。我还整理了描述表达式目录树的节点的节点类型System.Linq.Expressions. ExpressionType。
namespace System.Linq.Expressions
{
/// <summary>
/// 描述表达式目录树的节点的节点类型。
/// </summary>
public enum ExpressionType
{
/// <summary>
/// 加法运算,如 a + b,针对数值操作数,不进行溢出检查。
/// </summary>
Add = ,
//
/// <summary>
/// 加法运算,如 (a + b),针对数值操作数,进行溢出检查。
/// </summary>
AddChecked = ,
//
/// <summary>
/// 按位或逻辑 AND 运算,如 C# 中的 (a & b) 和 Visual Basic 中的 (a And b)。
/// </summary>
And = ,
//
/// <summary>
/// 条件 AND 运算,它仅在第一个操作数的计算结果为 true 时才计算第二个操作数。 它与 C# 中的 (a && b) 和 Visual Basic 中的 (a AndAlso b) 对应。
/// </summary>
AndAlso = ,
//
/// <summary>
/// 获取一维数组长度的运算,如 array.Length。
/// </summary>
ArrayLength = ,
//
/// <summary>
/// 一维数组中的索引运算,如 C# 中的 array[index] 或 Visual Basic 中的 array(index)。
/// </summary>
ArrayIndex = ,
//
/// <summary>
/// 方法调用,如在 obj.sampleMethod() 表达式中。
/// </summary>
Call = ,
//
/// <summary>
/// 表示 null 合并运算的节点,如 C# 中的 (a ?? b) 或 Visual Basic 中的 If(a, b)。
/// </summary>
Coalesce = ,
//
/// <summary>
/// 条件运算,如 C# 中的 a > b ? a : b 或 Visual Basic 中的 If(a > b, a, b)。
/// </summary>
Conditional = ,
//
/// <summary>
/// 一个常量值。
/// </summary>
Constant = ,
//
/// <summary>
/// 强制转换或转换运算,如 C#中的 (SampleType)obj 或 Visual Basic 中的 CType(obj, SampleType)。
/// 对于数值转换,如果转换后的值对于目标类型来说太大,这不会引发异常。
/// </summary>
Convert = ,
//
/// <summary>
/// 强制转换或转换运算,如 C#中的 (SampleType)obj 或 Visual Basic 中的 CType(obj, SampleType)。
/// 对于数值转换,如果转换后的值与目标类型大小不符,则引发异常。
/// </summary>
ConvertChecked = ,
//
/// <summary>
/// 除法运算,如 (a / b),针对数值操作数。
/// </summary>
Divide = ,
//
/// <summary>
/// 表示相等比较的节点,如 C# 中的 (a == b) 或 Visual Basic 中的 (a = b)。
/// </summary>
Equal = ,
//
/// <summary>
/// 按位或逻辑 XOR 运算,如 C# 中的 (a ^ b) 或 Visual Basic 中的 (a Xor b)。
/// </summary>
ExclusiveOr = ,
//
/// <summary>
/// “大于”比较,如 (a > b)。
/// </summary>
GreaterThan = ,
//
/// <summary>
/// “大于或等于”比较,如 (a >= b)。
/// </summary>
GreaterThanOrEqual = ,
//
/// <summary>
/// 调用委托或 lambda 表达式的运算,如 sampleDelegate.Invoke()。
/// </summary>
Invoke = ,
//
/// <summary>
/// lambda 表达式,如 C# 中的 a => a + a 或 Visual Basic 中的 Function(a) a + a。
/// </summary>
Lambda = ,
//
/// <summary>
/// 按位左移运算,如 (a << b)。
/// </summary>
LeftShift = ,
//
/// <summary>
/// “小于”比较,如 (a < b)。
/// </summary>
LessThan = ,
//
/// <summary>
/// “小于或等于”比较,如 (a <= b)。
/// </summary>
LessThanOrEqual = ,
//
/// <summary>
/// 创建新的 System.Collections.IEnumerable 对象并从元素列表中初始化该对象的运算,如 C# 中的 new List<SampleType>(){
/// a, b, c } 或 Visual Basic 中的 Dim sampleList = { a, b, c }。
/// </summary>
ListInit = ,
//
/// <summary>
/// 从字段或属性进行读取的运算,如 obj.SampleProperty。
/// </summary>
MemberAccess = ,
//
/// <summary>
/// 创建新的对象并初始化其一个或多个成员的运算,如 C# 中的 new Point { X = 1, Y = 2 } 或 Visual Basic 中的
/// New Point With {.X = 1, .Y = 2}。
/// </summary>
MemberInit = ,
//
/// <summary>
/// 算术余数运算,如 C# 中的 (a % b) 或 Visual Basic 中的 (a Mod b)。
/// </summary>
Modulo = ,
//
/// <summary>
/// 乘法运算,如 (a * b),针对数值操作数,不进行溢出检查。
/// </summary>
Multiply = ,
//
/// <summary>
/// 乘法运算,如 (a * b),针对数值操作数,进行溢出检查。
/// </summary>
MultiplyChecked = ,
//
/// <summary>
/// 算术求反运算,如 (-a)。 不应就地修改 a 对象。
/// </summary>
Negate = ,
//
/// <summary>
/// 一元加法运算,如 (+a)。 预定义的一元加法运算的结果是操作数的值,但用户定义的实现可以产生特殊结果。
/// </summary>
UnaryPlus = ,
//
/// <summary>
/// 算术求反运算,如 (-a),进行溢出检查。 不应就地修改 a 对象。
/// </summary>
NegateChecked = ,
//
/// <summary>
/// 调用构造函数创建新对象的运算,如 new SampleType()。
/// </summary>
New = ,
//
/// <summary>
/// 创建新的一维数组并从元素列表中初始化该数组的运算,如 C# 中的 new SampleType[]{a, b, c} 或 Visual Basic
/// 中的 New SampleType(){a, b, c}。
/// </summary>
NewArrayInit = ,
//
/// <summary>
/// 创建新数组(其中每个维度的界限均已指定)的运算,如 C# 中的 new SampleType[dim1, dim2] 或 Visual Basic
/// 中的 New SampleType(dim1, dim2)。
/// </summary>
NewArrayBounds = ,
//
/// <summary>
/// 按位求补运算或逻辑求反运算。 在 C# 中,它与整型的 (~a) 和布尔值的 (!a) 等效。 在 Visual Basic 中,它与 (Not
/// a) 等效。 不应就地修改 a 对象。
/// </summary>
Not = ,
//
/// <summary>
/// 不相等比较,如 C# 中的 (a != b) 或 Visual Basic 中的 (a <> b)。
/// </summary>
NotEqual = ,
//
/// <summary>
/// 按位或逻辑 OR 运算,如 C# 中的 (a | b) 或 Visual Basic 中的 (a Or b)。
/// </summary>
Or = ,
//
/// <summary>
/// 短路条件 OR 运算,如 C# 中的 (a || b) 或 Visual Basic 中的 (a OrElse b)。
/// </summary>
OrElse = ,
//
/// <summary>
/// 对在表达式上下文中定义的参数或变量的引用。 有关更多信息,请参见 System.Linq.Expressions.ParameterExpression。
/// </summary>
Parameter = ,
//
/// <summary>
/// 对某个数字进行幂运算的数学运算,如 Visual Basic 中的 (a ^ b)。
/// </summary>
Power = ,
//
/// <summary>
/// 具有类型为 System.Linq.Expressions.Expression 的常量值的表达式。 System.Linq.Expressions.ExpressionType.Quote
/// 节点可包含对参数的引用,这些参数在该节点表示的表达式的上下文中定义。
/// </summary>
Quote = ,
//
/// <summary>
/// 按位右移运算,如 (a >> b)。
/// </summary>
RightShift = ,
//
/// <summary>
/// 减法运算,如 (a - b),针对数值操作数,不进行溢出检查。
/// </summary>
Subtract = ,
//
/// <summary>
/// 算术减法运算,如 (a - b),针对数值操作数,进行溢出检查。
/// </summary>
SubtractChecked = ,
//
/// <summary>
/// 显式引用或装箱转换,其中如果转换失败则提供 null,如 C# 中的 (obj as SampleType) 或 Visual Basic 中的
/// TryCast(obj, SampleType)。
/// </summary>
TypeAs = ,
//
/// <summary>
/// 类型测试,如 C# 中的 obj is SampleType 或 Visual Basic 中的 TypeOf obj is SampleType。
/// </summary>
TypeIs = ,
//
/// <summary>
/// 赋值运算,如 (a = b)。
/// </summary>
Assign = ,
//
/// <summary>
/// 表达式块。
/// </summary>
Block = ,
//
/// <summary>
/// 调试信息。
/// </summary>
DebugInfo = ,
//
/// <summary>
/// 一元递减运算,如 C# 和 Visual Basic 中的 (a - 1)。 不应就地修改 a 对象。
/// </summary>
Decrement = ,
//
/// <summary>
/// 动态操作。
/// </summary>
Dynamic = ,
//
/// <summary>
/// 默认值。
/// </summary>
Default = ,
//
/// <summary>
/// 扩展表达式。
/// </summary>
Extension = ,
//
/// <summary>
/// “跳转”表达式,如 C# 中的 goto Label 或 Visual Basic 中的 GoTo Label。
/// </summary>
Goto = ,
//
/// <summary>
/// 一元递增运算,如 C# 和 Visual Basic 中的 (a + 1)。 不应就地修改 a 对象。
/// </summary>
Increment = ,
//
/// <summary>
/// 索引运算或访问使用参数的属性的运算。
/// </summary>
Index = ,
//
/// <summary>
/// 标签。
/// </summary>
Label = ,
//
/// <summary>
/// 运行时变量的列表。 有关更多信息,请参见 System.Linq.Expressions.RuntimeVariablesExpression。
/// </summary>
RuntimeVariables = ,
//
/// <summary>
/// 循环,如 for 或 while。
/// </summary>
Loop = ,
//
/// <summary>
/// 多分支选择运算,如 C# 中的 switch 或 Visual Basic 中的 Select Case。
/// </summary>
Switch = ,
//
/// <summary>
/// 引发异常的运算,如 throw new Exception()。
/// </summary>
Throw = ,
//
/// <summary>
/// try-catch 表达式。
/// </summary>
Try = ,
//
/// <summary>
/// 取消装箱值类型运算,如 MSIL 中的 unbox 和 unbox.any 指令。
/// </summary>
Unbox = ,
//
/// <summary>
/// 加法复合赋值运算,如 (a += b),针对数值操作数,不进行溢出检查。
/// </summary>
AddAssign = ,
//
/// <summary>
/// 按位或逻辑 AND 复合赋值运算,如 C# 中的 (a &= b)。
/// </summary>
AndAssign = ,
//
/// <summary>
/// 除法复合赋值运算,如 (a /= b),针对数值操作数。
/// </summary>
DivideAssign = ,
//
/// <summary>
/// 按位或逻辑 XOR 复合赋值运算,如 C# 中的 (a ^= b)。
/// </summary>
ExclusiveOrAssign = ,
//
/// <summary>
/// 按位左移复合赋值运算,如 (a <<= b)。
/// </summary>
LeftShiftAssign = ,
//
/// <summary>
/// 算术余数复合赋值运算,如 C# 中的 (a %= b)。
/// </summary>
ModuloAssign = ,
//
/// <summary>
/// 乘法复合赋值运算,如 (a *= b),针对数值操作数,不进行溢出检查。
/// </summary>
MultiplyAssign = ,
//
/// <summary>
/// 按位或逻辑 OR 复合赋值运算,如 C# 中的 (a |= b)。
/// </summary>
OrAssign = ,
//
/// <summary>
/// 对某个数字进行幂运算的复合赋值运算,如 Visual Basic 中的 (a ^= b)。
/// </summary>
PowerAssign = ,
//
/// <summary>
/// 按位右移复合赋值运算,如 (a >>= b)。
/// </summary>
RightShiftAssign = ,
//
/// <summary>
/// 减法复合赋值运算,如 (a -= b),针对数值操作数,不进行溢出检查。
/// </summary>
SubtractAssign = ,
//
/// <summary>
/// 加法复合赋值运算,如 (a += b),针对数值操作数,进行溢出检查。
/// </summary>
AddAssignChecked = ,
//
/// <summary>
/// 乘法复合赋值运算,如 (a *= b),针对数值操作数,进行溢出检查。
/// </summary>
MultiplyAssignChecked = ,
//
/// <summary>
/// 减法复合赋值运算,如 (a -= b),针对数值操作数,进行溢出检查。
/// </summary>
SubtractAssignChecked = ,
//
/// <summary>
/// 一元前缀递增,如 (++a)。 应就地修改 a 对象。
/// </summary>
PreIncrementAssign = ,
//
/// <summary>
/// 一元前缀递减,如 (--a)。 应就地修改 a 对象。
/// </summary>
PreDecrementAssign = ,
//
/// <summary>
/// 一元后缀递增,如 (a++)。 应就地修改 a 对象。
/// </summary>
PostIncrementAssign = ,
//
/// <summary>
/// 一元后缀递减,如 (a--)。 应就地修改 a 对象。
/// </summary>
PostDecrementAssign = ,
//
/// <summary>
/// 确切类型测试。
/// </summary>
TypeEqual = ,
//
/// <summary>
/// 二进制反码运算,如 C# 中的 (~a)。
/// </summary>
OnesComplement = ,
//
/// <summary>
/// true 条件值。
/// </summary>
IsTrue = ,
//
/// <summary>
/// false 条件值。
/// </summary>
IsFalse = ,
}
}
ExpressionType
在查询条件方面,要做的就是解析其中一些类型的表达式。
常用表达式举例
下面列举出常用的查询语句。
System.Linq.Expressions.Expression<Func<Cat, bool>> pre = null; pre = x => x.Price == ;
pre = x => x.Price < ;
pre = x => x.Price > ;
pre = x => x.Price < || x.Price > ;
pre = x => < x.Price && x.Price < ;
pre = x => x.KittyName.Contains("");
pre = x => x.KittyName.StartsWith("kitty");
pre = x => x.KittyName.EndsWith("");
数据库文件的逻辑结构
块(Block)
数据库文件中要保存所有的表(Table)信息、各个表(Table)的索引(Index)信息、各个索引下的Skip List结点(Skip List Node)信息、各个Skip List Node的key和value(这是所有的数据库记录对象所在的位置)信息和所有的数据库记录。我们为不同种类的的信息分别设计一个类型,称为XXXBlock,它们都继承抽象类型Block。我们还规定,不同类型的Block只能存放在相应类型的页里(只有一个例外)。这样似乎效率更高。
文件链表
一个数据库,会有多个表(Table)。数据库里的表的数量随时会增加减少。要想把多个表存储到文件里,以后还能全部读出来,最好使用链表结构。我们用TableBlock描述存储到数据库文件里的一个表(Table)。TableBlock是在文件中的一个链表结点,其NextPos是指向文件中的某个位置的指针。只要有NextPos,就可以反序列化出NextObj,也就是下一个TableBlock。我把这种在文件中存在的链表称为文件链表。以前所见所用的链表则是内存链表。
一个表里,会有多个索引(Index),类似的,IndexBlock也是一个文件链表。
SkipListNodeBlock存储的是Skip List的一个结点,而Skip List的结点有Down和Right两个指针,所以SkipListNodeBlock要存储两个指向文件中某处位置的指针DownPos和RightPos。就是说,SkipListNodeBlock是一个扩展了的文件链表。
了解了这些概念,就可以继续设计了。
Block
任何一个块,都必须知道自己应该存放到数据库文件的位置(ThisPos)。为了能够进行序列化和反序列化,都要有[Serializable]特性。为了控制序列化和反序列化过程,要实现ISerializable接口。
/// <summary>
/// 存储到数据库文件的一块内容。
/// </summary>
[Serializable]
public abstract class Block : ISerializable
{ #if DEBUG /// <summary>
/// 创建新<see cref="Block"/>时应设置其<see cref="Block.blockID"/>为计数器,并增长此计数器值。
/// </summary>
internal static long IDCounter = ; /// <summary>
/// 用于给此块标记一个编号,仅为便于调试之用。
/// </summary>
public long blockID;
#endif /// <summary>
/// 此对象自身在数据库文件中的位置。为0时说明尚未指定位置。只有<see cref="DBHeaderBlock"/>的位置才应该为0。
/// <para>请注意在读写时设定此值。</para>
/// </summary>
public long ThisPos { get; set; } /// <summary>
/// 存储到数据库文件的一块内容。
/// </summary>
public Block()
{
#if DEBUG
this.blockID = IDCounter++;
#endif
BlockCache.AddFloatingBlock(this);
} #if DEBUG
const string strBlockID = "";
#endif #region ISerializable 成员 /// <summary>
/// 序列化时系统会调用此方法。
/// </summary>
/// <param name="info"></param>
/// <param name="context"></param>
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
#if DEBUG
info.AddValue(strBlockID, this.blockID);
#endif
} #endregion /// <summary>
/// BinaryFormatter会通过调用此方法来反序列化此块。
/// </summary>
/// <param name="info"></param>
/// <param name="context"></param>
protected Block(SerializationInfo info, StreamingContext context)
{
#if DEBUG
this.blockID = info.GetInt64(strBlockID);
#endif
} /// <summary>
/// 显示此块的信息,便于调试。
/// </summary>
/// <returns></returns>
public override string ToString()
{
#if DEBUG
return string.Format("{0}: ID:{1}, Pos: {2}", this.GetType().Name, this.blockID, this.ThisPos);
#else
return string.Format("{0}: Pos: {1}", this.GetType().Name, this.ThisPos);
#endif
} /// <summary>
/// 安排所有文件指针。如果全部安排完毕,返回true,否则返回false。
/// </summary>
/// <returns></returns>
public abstract bool ArrangePos();
}
DBHeaderBlock
这是整个数据库的头部,用于保存在数据库范围内的全局变量。它在整个数据库中只有一个,并且放在数据库的第一页(0~4095字节里)。
TableBlock
TableBlock存放某种类型的表(Table)的信息,包括索引的头结点位置和下一个TableBlock的位置。前面说过,TableBlock是内存链表的一个结点。链表最好有个头结点,头结点不存储具有业务价值的数据,但是它会为编码提供方便。考虑到数据库的第一页只存放着一个DBHeaderBlock,我们就把TableBlock的头结点紧挨着放到DBHeaderBlock后面。这就是上面所说的唯一的例外。由于TableBlock的头结点不会移动位置,其序列化后的字节数也不会变,所以放这里是没有问题的。
IndexBlock
IndexBlock存储Table的一个索引。IndexBlock也是内存链表的一个结点。而它内部含有指向SkipListNodeBlock的指针,所以,实际上IndexBlock就充当了SkipList。
SkipListNodeBlock
如前所述,这是一个扩展了的文件链表的结点。此结点的key和value都是指向实际数据的文件指针。如果直接保存实际数据,那么每个结点都保存一份完整的数据会造成很大的浪费。特别是value,如果value里有一个序列化了的图片,那是不可想象的。而且这样一来,所有的SkipListNodeBlock序列化的长度都是相同的。
DataBlock
DataBlock也是文件链表的一个结点。由于某些数据库记录会很大(比如要存储一个System.Drawing.Image),一个页只有4KB,无法放下。所以可能需要把一条记录划分为多个数据块,放到多个DataBlock里。也就是说,一个数据库记录是用一个链表保存的。
PageHeaderBlock
为了以后管理页(申请新页、释放不再使用的页、申请一定长度的空闲空间),我们在每个页的起始位置都放置一个PageHeaderBlock,用来保存此页的状态(可用字节数等)。并且,每个页都包含一个指向下一相同类型的页的位置的文件指针。这样,所有存放TableBlock的页就成为一个文件链表,所有存放IndexBlock的页就成为另一个文件链表,所有存放SkipListNodeBlock的页也成为一个文件链表,所有存放DataBlock的页也一样。
另外,在删除某些记录后,有的页里存放的块可能为0,这时就成为一个空白页(Empty Page),所以我们还要把这些空白页串联成一个文件链表。
总之,文件里的链表关系无处不在。
块(Block)在数据库文件里
下面是我画的一个数据库文件的逻辑上的结构图。它展示了各种类型的块在数据库文件里的生存状态。
首先,刚刚创建一个数据库文件时,文件里是这样的:
当前只有一个DBHeaderBlock和一个TableBlock(作为头结点)。
注意:此时我们忽略"页"这个概念,所以在每个页最开始处的PageHeaderBlock就不考虑了。
之后我们Insert一条记录,这会在数据库里新建一个表及其索引信息,然后插入此记录。指向完毕后,数据库文件里就是这样的。
之前的TableBlock头结点指向了新建的TableBlock的位置,新建的TableBlock创建了自己的索引。
索引有两个结点,上面的那个是索引的头结点,其不包含有业务价值的信息,只指向下一个索引结点。
下一个索引结点是第一个有业务意义的索引结点,也是一个存在于文件中的SkipList,它有自己的一群SkipListNodeBlock。在插入第一条记录前,这群SkipListNodeBlock只有竖直方向的那5个(实际上我在数据库文件里设定的是32个,不过没法画那么多,就用5个指代了)。
现在表和索引创建完毕,开始插入第一条记录。这会随机创建若干个(这里是2个)SkipListNodeBlock(这是Skip List数据结构的特性,具体请参考*。这两个SkipListNodeBlock的keyPos和valuePos都指向了key和value所在的DataBlock的位置。用于存储value的DataBlock有2个结点,说明value(数据库记录序列化后的字节数)比较大,一个页占不下。
这就是我们期望的情况。为了实现这种文件链表,还需要后续一些遍历操作。我们将结合事务来完成它。
如果你感兴趣,下面是继续插入第二条记录后的情况:
注:为了避免图纸太乱,我只画出了最下面的K1, V1和K2, V2的指针指向DataBlock。实际上,各个K1, V1和K2, V2都是指向DataBlock的。
为块(Block)安排其在文件中的位置
根据依赖关系依次分配
新创建一个Block时,其在数据库文件中的位置(Block.ThisPos)都没有指定,那么在其它Block中指向它的那些字段/属性值就无法确定。我们通过两个步骤来解决此问题。
首先,我们给每个文件链表结点的NextPos都配备一个对应的NextObj。就是说,新创建的Block虽然在文件链表方面还没有安排好指针,但是在内存链表方面已经安排好了。
然后,等所需的所有Block都创建完毕,遍历这些Block,那些在内存链表中处于最后一个的结点,其字段/属性值不依赖其它Block的位置,因此可以直接为其分配好在文件里的位置和空间。之后再次遍历这些Block,那些依赖最后一个结点的结点,此时也就可以为其设置字段/属性值了。以此类推,多次遍历,直到所有Block的字段/属性值都设置完毕。
// 给所有的块安排数据库文件中的位置。
List<Block> arrangedBlocks = new List<Block>();
while (arrangedBlocks.Count < this.blockList.Count)
{
for (int i = this.blockList.Count - ; i >= ; i--)// 后加入列表的先处理。
{
Block block = this.blockList[i];
if (arrangedBlocks.Contains(block)) { continue; }
bool done = block.ArrangePos();
if (done)
{
if (block.ThisPos == )
{
byte[] bytes = block.ToBytes();
if (bytes.Length > Consts.maxAvailableSpaceInPage)
{ throw new Exception("Block size is toooo large!"); }
AllocPageTypes pageType = block.BelongedPageType();
AllocatedSpace space = this.fileDBContext.Alloc(bytes.LongLength, pageType);
block.ThisPos = space.position;
} arrangedBlocks.Add(block);
}
}
}
我们要为不同类型的块执行各自的字段/属性值的设置方法,通过继承Block基类的abstract bool ArrangePos();来实现。实际上,只要添加到blockList里的顺序得当,只需一次遍历即可完成所有的自动/属性值的设置。
DBHeaderBlock
此类型的字段/属性值不依赖其它任何Block,永远都是实时分配完成的。
internal override bool ArrangePos()
{
return true;// 此类型比较特殊,应该在更新时立即指定各项文件指针。
}
TableBlock
作为头结点的那个TableBlock不含索引,因此其字段/属性值不需设置。其它TableBlock则需要保存索引头结点的位置。
作为链表的最后一个结点的那个TableBlock的字段/属性值不依赖其它TableBlock的位置,其它TableBLock则需要其下一个TableBlock的位置。这一规则对每个文件链表都适用。
public override bool ArrangePos()
{
bool allArranged = true; if (this.IndexBlockHead != null)// 如果IndexBlockHead == null,则说明此块为TableBlock的头结点。头结点是不需要持有索引块的。
{
if (this.IndexBlockHead.ThisPos != )
{ this.IndexBlockHeadPos = this.IndexBlockHead.ThisPos; }
else
{ allArranged = false; }
} if (this.NextObj != null)
{
if (this.NextObj.ThisPos != )
{ this.NextPos = this.NextObj.ThisPos; }
else
{ allArranged = false; }
} return allArranged;
}
IndexBlock
IndexBlock也是文件链表的一个结点,其字段/属性值的设置方式与TableBlock相似。
public override bool ArrangePos()
{
bool allArranged = true; if (this.SkipListHeadNodes != null)// 如果这里的SkipListHeadNodes == null,则说明此索引块是索引链表里的头结点。头结点是不需要SkipListHeadNodes有数据的。
{
int length = this.SkipListHeadNodes.Length;
if (length == )
{ throw new Exception("SKip List's head nodes has 0 element!"); }
long pos = this.SkipListHeadNodes[length - ].ThisPos;
if (pos != )
{ this.SkipListHeadNodePos = pos; }
else
{ allArranged = false; }
} if (this.SkipListTailNode != null)// 如果这里的SkipListTailNodes == null,则说明此索引块是索引链表里的头结点。头结点是不需要SkipListTailNodes有数据的。
{
long pos = this.SkipListTailNode.ThisPos;
if (pos != )
{ this.SkipListTailNodePos = pos; }
else
{ allArranged = false; }
} if (this.NextObj != null)
{
if (this.NextObj.ThisPos != )
{ this.NextPos = this.NextObj.ThisPos; }
else
{ allArranged = false; }
} return allArranged;
}
SkipListNodeBlock
SkipListNodeBlock是扩展了的文件链表,关于指向下一结点的指针的处理与前面类似。作为头结点的SkipListNodeBlock的Key和Value都是null,是不依赖其它Block的。非头结点的SkipListNodeBlock的Key和Value则依赖保存着序列化后的Key和Value的DataBlock。
public override bool ArrangePos()
{
bool allArranged = true; if (this.Key != null)
{
if (this.Key.ThisPos != )
{ this.KeyPos = this.Key.ThisPos; }
else
{ allArranged = false; }
} if (this.Value != null)
{
if (this.Value[].ThisPos != )
{ this.ValuePos = this.Value[].ThisPos; }
else
{ allArranged = false; }
} if (this.DownObj != null)// 此结点不是最下方的结点。
{
if (this.DownObj.ThisPos != )
{ this.DownPos = this.DownObj.ThisPos; }
else
{ allArranged = false; }
} if (this.RightObj != null)// 此结点不是最右方的结点。
{
if (this.RightObj.ThisPos != )
{ this.RightPos = this.RightObj.ThisPos; }
else
{ allArranged = false; }
} return allArranged;
}
DataBlock
DataBlock就是一个很单纯的文件链表结点。
public override bool ArrangePos()
{
bool allArranged = true; if (NextObj != null)
{
if (NextObj.ThisPos != )
{ this.NextPos = NextObj.ThisPos; }
else
{ allArranged = false; }
} return allArranged;
}
PageHeaderBlock
此类型只在创建新页和申请已有页空间时出现,不会参与上面各类Block的位置分配,而是在创建时就为其安排好NextPagePos等属性。
internal override bool ArrangePos()
{
return true;// 此类型比较特殊,应该在创建时就为其安排好NextPagePos等属性。
}
Demo和工具
为了方便调试和使用SharpFileDB,我做了下面这些工具和示例Demo。
Demo:MyNote
这是一个非常简单的Demo,实现一个简单的便签列表,演示了如何使用SharpFileDB。虽然界面不好看,但是用作Demo也不应该有太多的无关代码。
查看数据库状态
为了能够直观地查看数据库的状态(包含哪些表、索引、数据记录),方便调试,这里顺便提供一个Winform程序"SharpFileDB Viewer"。这样可以看到数据库的几乎全部信息,调试起来就方便多了。
点击"Refresh"会重新加载所有的表、索引和记录信息,简单粗暴。
点击"Skip Lists"会把数据库里所有表的所有索引的所有结点和所有数据都画到BMP图片上。比如上面看到的MyNote.db的索引分别情况如下图所示。
此图绘制了Note表的唯一一个索引和表里的全部记录(共10条)。
由于为Skip List指定了最大层数为8,且现在数据库里只有10条记录,所以图示上方比较空旷。
下面是局部视图,看得比较清楚。
点击"Blocks"会把数据库各个页上包含的各个块的占用情况画到多个宽为4096高为100的BMP图片上。例如下面6张图是添加了一个表、一个索引和一条记录后,数据库文件的全部6个页的情况。
如上图所示,每一页头部都含有一个PageHeaderBlock,用浅绿色表示。
第一页还有一个数据库头部DBHeaderBlock(用青紫色表示)和一个TableBlock头结点(用橙色表示)。第六页也有一个TableBlock,它代表目前数据库的唯一一个表。Table头结点的TableType属性是空的,所以其长度比第六页的TableBlock要短。这些图片是完全准确地依照各个Block在数据库文件中存储的位置和长度绘制的。
第二页是2个DataBlock,用金色表示。(DataBlock里存放的才是有业务价值的数据,所以是金子的颜色)
第三、第四页存放的都是SkipListNodeBlock(用绿色表示)。可见索引是比较占地方的。
第五页存放了两个IndexBlock(其中一个是IndexBlock的头结点),用灰色表示。
这些图片就比前面手工画的草图好看多了。
以后有时间我把这些图片画到Form上,当鼠标停留在一个块上时,会显示此块的具体信息(如位置、长度、类型、相关联的块等),那就更方便监视数据库的状态了。
后续我会根据需要增加显示更多信息的功能。
可视化的数据库设计器
如果能像MSSQLServer那样用可视化的方法创建自定义表,做成自己的数据库,那就最好了。所以我写了这样一个可视化的数据库设计器,你可以用可视化方式设计你的数据库,然后一键生成所有代码。
规则/坑
各种各样的库、工具包都有一些隐晦的规则,一旦触及就会引发各种奇怪的问题。这种规则其实就是坑。SharpFileDB尽可能不设坑或不设深坑。那些实在无法阻挡的坑,我就让它浅浅地、明显地存在着,即使掉进去了也能马上跳出来。另外,通过使用可视化的设计器,还可以自动避免掉坑里,因为设计器在生成代码前是会提醒你是否已经一脚进坑了。
SharpFileDB有如下几条规则(几个坑)你需要知道:
继承Table的表类型序列化后不能太大
你设计的Table类型序列化后的长度不能超过Int32.MaxValue个字节(= 2097151KB = 2047MB ≈2GB)。这个坑够浅了吧。如果一个对象能占据2GB,那早就不该用SharpFileDB了。所以这个坑应该不会有人踩到。
只能在属性上添加索引
索引必须加在属性上,否则无效。为什么呢?因为我在实现SharpFileDB的时候只检测了表类型的各个PropertyInfo是否附带[TableIndex]特性。我想,这算是人为赋予属性的一种荣誉吧。
目前为止,这两个坑算是最深的了。但愿它们的恶劣影响不大。
剩下的几个坑是给SharpFileDB开发者的(目前就是我)。我会在各种可能发现bug的地方直接抛出异常。等这些throw消停了,就说明SharpFileDB稳定了。我再想个办法把这些难看的throw收拾收拾。
总结
现在的SharpFileDB很多方面(编号、分页、索引、查询、块)都受到LiteDB的启示,再次表示感谢。
您可以(到我的Github上浏览此项目)。
也许我做的这个SharpFileDB很幼稚,不过我相信它可以用于实践,我也尽量写了注释、提供了最简单的使用方法,还提供了Demo。还是那句话,欢迎您指出我的代码有哪些不足,我应该学习补充哪些知识,但请不要无意义地谩骂和人身攻击。