Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/bitzhuwei/sharpfiledb

SharpFileDB is a micro database library that uses no SQL, files as storage form and totally C# to implement a CRUD system. SharpFileDB是一个纯C#的无SQL的支持CRUD的小型文件数据库,目标是支持万人级别的应用程序。
https://github.com/bitzhuwei/sharpfiledb

Last synced: about 1 month ago
JSON representation

SharpFileDB is a micro database library that uses no SQL, files as storage form and totally C# to implement a CRUD system. SharpFileDB是一个纯C#的无SQL的支持CRUD的小型文件数据库,目标是支持万人级别的应用程序。

Awesome Lists containing this project

README

        

# SharpFileDB
SharpFileDB is a micro database library that uses no SQL, files as storage form and totally C# to implement a CRUD system.
SharpFileDB是一个纯C#的无SQL的支持CRUD的小型文件数据库,目标是支持万人级别的应用程序。

小型单文件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、可视化的监视工具、可视化的数据库设计器,便于学习、调试和应用。


 


使用场景



假设已经做好了这样一个单文件数据库,我期望的使用方式是这样的:



 1             string fullname = Path.Combine(Environment.CurrentDirectory, "test.db");

2 using (FileDBContext db = new FileDBContext(fullname))
3 {
4 Cat cat = new Cat();
5 string name = "kitty " + random.Next();
6 cat.KittyName = name;
7 cat.Price = random.Next(1, 100);
8
9 db.Insert(cat);
10
11 System.Linq.Expressions.Expression<Func<Cat, bool>> pre = null;
12
13 pre = (x =>
14 (x.KittyName == "kitty" || (x.KittyName == name && x.Id.ToString() != string.Empty))
15 || (x.KittyName.Contains("kitty") && x.Price > 10)
16 );
17
18 IEnumerable<Cat> cats = db.Find<Cat>(pre);
19
20 cats = db.FindAll<Cat>();
21
22 cat.KittyName = "小白 " + random.Next();
23 db.Update(cat);
24
25 db.Delete(cat);
26 }


 


就像关系型数据库一样,我们可以创建各种Table(例如这里的Cat)。然后直接使用Insert(Table record);插入一条记录。创建自定义Table只需继承Talbe实现自己的class即可。



 1     /// <summary>

2 /// 继承此类型以实现您需要的Table。
3 /// </summary>
4 [Serializable]
5 public abstract class Table : ISerializable
6 {
7
8 /// <summary>
9 /// 用以区分每个Table的每条记录。
10 /// This Id is used for diffrentiate instances of 'table's.
11 /// </summary>
12 [TableIndex]// 标记为索引,这是每个表都有的主键。
13 public ObjectId Id { get; internal set; }
14
15 /// <summary>
16 /// 创建一个文件对象,在用<code>FileDBContext.Insert();</code>将此对象保存到数据库之前,此对象的Id为null。
17 /// </summary>
18 public Table() { }
19
20 /// <summary>
21 /// 显示此条记录的Id。
22 /// </summary>
23 /// <returns></returns>
24 public override string ToString()
25 {
26 return string.Format("Id: {0}", this.Id);
27 }
28
29 /// <summary>
30 /// 使用的字符越少,序列化时占用的字节就越少。一个字符都不用最好。
31 /// <para>Using less chars means less bytes after serialization. And "" is allowed.</para>
32 /// </summary>
33 const string strId = "";
34
35 #region ISerializable 成员
36
37 /// <summary>
38 /// This method will be invoked automatically when IFormatter.Serialize() is called.
39 /// <para>You must use <code>base(info, context);</code> in the derived class to feed <see cref="Table"/>'s fields and properties.</para>
40 /// <para>当使用IFormatter.Serialize()时会自动调用此方法。</para>
41 /// <para>继承此类型时,必须在子类型中用<code>base(info, context);</code>来填充<see cref="Table"/>自身的数据。</para>
42 /// </summary>
43 /// <param name="info"></param>
44 /// <param name="context"></param>
45 public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
46 {
47 byte[] value = this.Id.Value;//byte[]比this.Id.ToString()占用的字节少2个字节。
48 info.AddValue(strId, value);
49 }
50
51 #endregion
52
53 /// <summary>
54 /// This method will be invoked automatically when IFormatter.Serialize() is called.
55 /// <para>You must use <code>: base(info, context)</code> in the derived class to feed <see cref="Table"/>'s fields and properties.</para>
56 /// <para>当使用IFormatter.Serialize()时会自动调用此方法。</para>
57 /// <para>继承此类型时,必须在子类型中用<code>: base(info, context)</code>来填充<see cref="Table"/>自身的数据。</para>
58 /// </summary>
59 /// <param name="info"></param>
60 /// <param name="context"></param>
61 protected Table(SerializationInfo info, StreamingContext context)
62 {
63 byte[] value = (byte[])info.GetValue(strId, typeof(byte[]));
64 this.Id = new ObjectId(value);
65 }
66
67 }


 


这里的Cat定义如下:


 



 1     [Serializable]

2 public class Cat : Table
3 {
4 /// <summary>
5 /// 显示此对象的信息,便于调试。
6 /// </summary>
7 /// <returns></returns>
8 public override string ToString()
9 {
10 return string.Format("{0}: ¥{1}", KittyName, Price);
11 }
12
13 public string KittyName { get; set; }
14
15 public int Price { get; set; }
16
17 public Cat() { }
18
19 const string strKittyName = "N";
20 const string strPrice = "P";
21
22 public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
23 {
24 base.GetObjectData(info, context);
25
26 info.AddValue(strKittyName, this.KittyName);
27 info.AddValue(strPrice, this.Price);
28 }
29
30 protected Cat(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
31 : base(info, context)
32 {
33 this.KittyName = info.GetString(strKittyName);
34 this.Price = info.GetInt32(strPrice);
35 }
36
37 }


 


后面我提供了一个可视化的数据库设计器,你可以像在SQL Server Management里那样设计好你需要的表,即可一键生成相应的数据库项目源码。


 


从何开始



用C#做一个小型单文件数据库,需要用到.NET Framework提供的这几个类型。


FileStream



文件流用于操作数据库文件。FileStream支持随机读写,并且FileStream.Length属性是long型的,就是说数据库文件最大可以有long.MaxValue个字节,这是超级大的。


使用FileStream的方式是这样的:



1 var fileStream = new FileStream(fullname, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);


 


这句代码指明:


fullname:打开绝对路径为fullname的文件。


FileMode.Open:如果文件不存在,抛出异常。


FileAccess.ReadWrite:fileStream对象具有读和写文件的权限。


FileShare.Read:其它进程只能读此文件,不能写。我们可以用其它进程来实现容灾备份之类的操作。


 


BinaryFormatter



读写数据库文件实际上就是反序列化和序列化对象的过程。我在这里详细分析了为什么使用BinaryFormatter


联合使用FileStream和BinaryFormatter就可以实现操作数据库文件的最基础的功能。



 1         /// <summary>

2 /// 使用FileStream和BinaryFormatter做单文件数据库的核心工作流。
3 /// </summary>
4 /// <param name="fullname"></param>
5 public static void TypicalScene(string fullname)
6 {
7 // 初始化。
8 BinaryFormatter formatter = new BinaryFormatter();
9
10 // 打开数据库文件。
11 FileStream fs = new FileStream(fullname, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
12
13 // 把对象写入数据库。
14 long position = 0;// 指定位置。
15 fs.Seek(position, SeekOrigin.Begin);
16 Object obj = new Object();// 此处可以是任意具有[Serializable]特性的类型。
17 formatter.Serialize(fs, obj);// 把对象序列化并写入文件。
18
19 fs.Flush();
20
21 // 从数据库文件读取对象。
22 fs.Seek(position, SeekOrigin.Begin);// 指定位置。
23 Object deserialized = formatter.Deserialize(fs);// 从文件得到反序列化的对象。
24
25 // 关闭文件流,退出数据库。
26 fs.Close();
27 fs.Dispose();
28 }


 


简单来说,这就是整个单文件数据库最基本的工作过程。后续的所有设计,目的都在于得到应指定的位置和应读写的对象类型了。能够在合适的位置写入合适的内容,能够通过索引实现快速定位和获取/删除指定的内容,这就是实现单文件数据库要做的第一步。能够实现事务和恢复机制,就是第二步。


 


MemoryStream



使用MemoryStream是为了先把对象转换成byte[],这样就可以计算其序列化后的长度,然后才能为其安排存储到数据库文件的什么地方。



 1         /// <summary>

2 /// 把Table的一条记录转换为字节数组。这个字节数组应该保存到Data页。
3 /// </summary>
4 /// <param name="table"></param>
5 /// <returns></returns>
6 [MethodImpl(MethodImplOptions.AggressiveInlining)]
7 public static byte[] ToBytes(this Table table)
8 {
9 byte[] result;
10 using (MemoryStream ms = new MemoryStream())
11 {
12 Consts.formatter.Serialize(ms, table);
13 if (ms.Length > (long)int.MaxValue)// RULE: 一条记录序列化后最长不能超过int.MaxValue个字节。
14 { throw new Exception(string.Format("Toooo long is the [{0}]", table)); }
15 result = new byte[ms.Length];
16 ms.Seek(0, SeekOrigin.Begin);
17 ms.Read(result, 0, result.Length);
18 }
19
20 return result;
21 }


 


 


准备知识


全局唯一编号



写入数据库的每一条记录,都应该有一个全局唯一的编号。(C#实现文件数据库)和(LiteDB)都有一个ObjectId类型,两者也十分相似,且存储它需要的长度也小于.NET Framework自带的Guid,所以就用ObjectId做全局唯一的编号了。





  1     /// <summary>

2 /// 用于生成唯一的<see cref="Table"/>编号。
3 /// </summary>
4 [Serializable]
5 public sealed class ObjectId : ISerializable, IComparable<ObjectId>, IComparable
6 {
7 private string _string;
8
9 private ObjectId()
10 {
11 }
12
13 internal ObjectId(string value)
14 : this(DecodeHex(value))
15 {
16 }
17
18 internal ObjectId(byte[] value)
19 {
20 Value = value;
21 }
22
23 internal static ObjectId Empty
24 {
25 get { return new ObjectId("000000000000000000000000"); }
26 }
27
28 internal byte[] Value { get; private set; }
29
30 /// <summary>
31 /// 获取一个新的<see cref="ObjectId"/>
32 /// </summary>
33 /// <returns></returns>
34 public static ObjectId NewId()
35 {
36 return new ObjectId { Value = ObjectIdGenerator.Generate() };
37 }
38
39 internal static bool TryParse(string value, out ObjectId objectId)
40 {
41 objectId = Empty;
42 if (value == null || value.Length != 24)
43 {
44 return false;
45 }
46
47 try
48 {
49 objectId = new ObjectId(value);
50 return true;
51 }
52 catch (FormatException)
53 {
54 return false;
55 }
56 }
57
58 static byte[] DecodeHex(string value)
59 {
60 if (string.IsNullOrEmpty(value))
61 throw new ArgumentNullException("value");
62
63 var chars = value.ToCharArray();
64 var numberChars = chars.Length;
65 var bytes = new byte[numberChars / 2];
66
67 for (var i = 0; i < numberChars; i += 2)
68 {
69 bytes[i / 2] = Convert.ToByte(new string(chars, i, 2), 16);
70 }
71
72 return bytes;
73 }
74
75 /// <summary>
76 ///
77 /// </summary>
78 /// <returns></returns>
79 public override int GetHashCode()
80 {
81 return Value != null ? ToString().GetHashCode() : 0;
82 }
83
84 /// <summary>
85 /// 显示此对象的信息,便于调试。
86 /// </summary>
87 /// <returns></returns>
88 public override string ToString()
89 {
90 if (_string == null && Value != null)
91 {
92 _string = BitConverter.ToString(Value)
93 .Replace("-", string.Empty)
94 .ToLowerInvariant();
95 }
96
97 return _string;
98 }
99
100 /// <summary>
101 ///
102 /// </summary>
103 /// <param name="obj"></param>
104 /// <returns></returns>
105 public override bool Equals(object obj)
106 {
107 var other = obj as ObjectId;
108 return Equals(other);
109 }
110
111 /// <summary>
112 ///
113 /// </summary>
114 /// <param name="other"></param>
115 /// <returns></returns>
116 public bool Equals(ObjectId other)
117 {
118 return other != null && ToString() == other.ToString();
119 }
120
121 //public static implicit operator string(ObjectId objectId)
122 //{
123 // return objectId == null ? null : objectId.ToString();
124 //}
125
126 //public static implicit operator ObjectId(string value)
127 //{
128 // return new ObjectId(value);
129 //}
130
131 /// <summary>
132 ///
133 /// </summary>
134 /// <param name="left"></param>
135 /// <param name="right"></param>
136 /// <returns></returns>
137 public static bool operator ==(ObjectId left, ObjectId right)
138 {
139 if (ReferenceEquals(left, right))
140 {
141 return true;
142 }
143
144 if (((object)left == null) || ((object)right == null))
145 {
146 return false;
147 }
148
149 return left.Equals(right);
150 }
151
152 /// <summary>
153 ///
154 /// </summary>
155 /// <param name="left"></param>
156 /// <param name="right"></param>
157 /// <returns></returns>
158 public static bool operator !=(ObjectId left, ObjectId right)
159 {
160 return !(left == right);
161 }
162
163 #region ISerializable 成员
164
165 const string strValue = "";
166 void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
167 {
168 string value = this.ToString();
169 info.AddValue(strValue, value);
170 }
171
172 #endregion
173
174 private ObjectId(SerializationInfo info, StreamingContext context)
175 {
176 string value = info.GetString(strValue);
177 this.Value = DecodeHex(value);
178 }
179
180
181 #region IComparable<ObjectId> 成员
182
183 /// <summary>
184 /// 根据<see cref="ObjectId.ToString()"/>的值比较两个对象。
185 /// </summary>
186 /// <param name="other"></param>
187 /// <returns></returns>
188 public int CompareTo(ObjectId other)
189 {
190 if (other == null) { return 1; }
191
192 string thisStr = this.ToString();
193 string otherStr = other.ToString();
194 int result = thisStr.CompareTo(otherStr);
195
196 return result;
197 }
198
199 #endregion
200
201 #region IComparable 成员
202
203 /// <summary>
204 /// 根据<see cref="ObjectId.ToString()"/>的值比较两个对象。
205 /// </summary>
206 /// <param name="obj"></param>
207 /// <returns></returns>
208 public int CompareTo(object obj)
209 {
210 ObjectId other = obj as ObjectId;
211 return CompareTo(other);
212 }
213
214 #endregion
215 }
216
217 internal static class ObjectIdGenerator
218 {
219 private static readonly DateTime Epoch =
220 new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
221 private static readonly object _innerLock = new object();
222 private static int _counter;
223 private static readonly byte[] _machineHash = GenerateHostHash();
224 private static readonly byte[] _processId =
225 BitConverter.GetBytes(GenerateProcessId());
226
227 internal static byte[] Generate()
228 {
229 var oid = new byte[12];
230 var copyidx = 0;
231
232 Array.Copy(BitConverter.GetBytes(GenerateTime()), 0, oid, copyidx, 4);
233 copyidx += 4;
234
235 Array.Copy(_machineHash, 0, oid, copyidx, 3);
236 copyidx += 3;
237
238 Array.Copy(_processId, 0, oid, copyidx, 2);
239 copyidx += 2;
240
241 Array.Copy(BitConverter.GetBytes(GenerateCounter()), 0, oid, copyidx, 3);
242
243 return oid;
244 }
245
246 private static int GenerateTime()
247 {
248 var now = DateTime.UtcNow;
249 var nowtime = new DateTime(Epoch.Year, Epoch.Month, Epoch.Day,
250 now.Hour, now.Minute, now.Second, now.Millisecond);
251 var diff = nowtime - Epoch;
252 return Convert.ToInt32(Math.Floor(diff.TotalMilliseconds));
253 }
254
255 private static byte[] GenerateHostHash()
256 {
257 using (var md5 = MD5.Create())
258 {
259 var host = Dns.GetHostName();
260 return md5.ComputeHash(Encoding.Default.GetBytes(host));
261 }
262 }
263
264 private static int GenerateProcessId()
265 {
266 var process = Process.GetCurrentProcess();
267 return process.Id;
268 }
269
270 private static int GenerateCounter()
271 {
272 lock (_innerLock)
273 {
274 return _counter++;
275 }
276 }
277 }


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表达式的解析。



 1         /// <summary>

2 /// 查找数据库内的某些记录。
3 /// </summary>
4 /// <typeparam name="T"></typeparam>
5 /// <param name="predicate">符合此条件的记录会被取出。</param>
6 /// <returns></returns>
7 public IEnumerable<T> Find<T>(Expression<Func<T, bool>> predicate) where T : Table, new()
8 {
9 if (predicate == null) { throw new ArgumentNullException("predicate"); }
10
11 // 这是没有利用索引的版本。
12 Func<T, bool> func = predicate.Compile();
13 foreach (T item in this.FindAll<T>())
14 {
15 if(func(item))
16 {
17 yield return item;
18 }
19 }
20
21 // TODO: 这是利用索引的版本,尚未实现。
22 //List<T> result = new List<T>();
23
24 //var body = predicate.Body as LambdaExpression;
25 //this.Find(result, body);
26
27 //return result;
28 }
29 /// <summary>
30 /// 查找数据库内所有指定类型的记录。
31 /// </summary>
32 /// <typeparam name="T">要查找的类型。</typeparam>
33 /// <returns></returns>
34 public IEnumerable<T> FindAll<T>() where T:Table, new()
35 {
36 Type type = typeof(T);
37 if (this.tableBlockDict.ContainsKey(type))
38 {
39 TableBlock tableBlock = this.tableBlockDict[type];
40 IndexBlock firstIndex = tableBlock.IndexBlockHead.NextObj;// 第一个索引应该是Table.Id的索引。
41 FileStream fs = this.fileStream;
42
43 SkipListNodeBlock current = firstIndex.SkipListHeadNodes[0]; //currentHeadNode;
44
45 while (current.RightPos != firstIndex.SkipListTailNodePos)
46 {
47 current.TryLoadProperties(fs, SkipListNodeBlockLoadOptions.RightObj);
48 current.RightObj.TryLoadProperties(fs, SkipListNodeBlockLoadOptions.RightObj | SkipListNodeBlockLoadOptions.Value);
49 T item = current.RightObj.Value.GetObject<T>(this);
50
51 yield return item;
52
53 current = current.RightObj;
54 }
55 }
56 }


 


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。





  1 namespace System.Linq.Expressions

2 {
3 /// <summary>
4 /// 描述表达式目录树的节点的节点类型。
5 /// </summary>
6 public enum ExpressionType
7 {
8 /// <summary>
9 /// 加法运算,如 a + b,针对数值操作数,不进行溢出检查。
10 /// </summary>
11 Add = 0,
12 //
13 /// <summary>
14 /// 加法运算,如 (a + b),针对数值操作数,进行溢出检查。
15 /// </summary>
16 AddChecked = 1,
17 //
18 /// <summary>
19 /// 按位或逻辑 AND 运算,如 C# 中的 (a & b) 和 Visual Basic 中的 (a And b)。
20 /// </summary>
21 And = 2,
22 //
23 /// <summary>
24 /// 条件 AND 运算,它仅在第一个操作数的计算结果为 true 时才计算第二个操作数。 它与 C# 中的 (a && b) 和 Visual Basic 中的 (a AndAlso b) 对应。
25 /// </summary>
26 AndAlso = 3,
27 //
28 /// <summary>
29 /// 获取一维数组长度的运算,如 array.Length。
30 /// </summary>
31 ArrayLength = 4,
32 //
33 /// <summary>
34 /// 一维数组中的索引运算,如 C# 中的 array[index] 或 Visual Basic 中的 array(index)。
35 /// </summary>
36 ArrayIndex = 5,
37 //
38 /// <summary>
39 /// 方法调用,如在 obj.sampleMethod() 表达式中。
40 /// </summary>
41 Call = 6,
42 //
43 /// <summary>
44 /// 表示 null 合并运算的节点,如 C# 中的 (a ?? b) 或 Visual Basic 中的 If(a, b)。
45 /// </summary>
46 Coalesce = 7,
47 //
48 /// <summary>
49 /// 条件运算,如 C# 中的 a > b ? a : b 或 Visual Basic 中的 If(a > b, a, b)。
50 /// </summary>
51 Conditional = 8,
52 //
53 /// <summary>
54 /// 一个常量值。
55 /// </summary>
56 Constant = 9,
57 //
58 /// <summary>
59 /// 强制转换或转换运算,如 C#中的 (SampleType)obj 或 Visual Basic 中的 CType(obj, SampleType)。
60 /// 对于数值转换,如果转换后的值对于目标类型来说太大,这不会引发异常。
61 /// </summary>
62 Convert = 10,
63 //
64 /// <summary>
65 /// 强制转换或转换运算,如 C#中的 (SampleType)obj 或 Visual Basic 中的 CType(obj, SampleType)。
66 /// 对于数值转换,如果转换后的值与目标类型大小不符,则引发异常。
67 /// </summary>
68 ConvertChecked = 11,
69 //
70 /// <summary>
71 /// 除法运算,如 (a / b),针对数值操作数。
72 /// </summary>
73 Divide = 12,
74 //
75 /// <summary>
76 /// 表示相等比较的节点,如 C# 中的 (a == b) 或 Visual Basic 中的 (a = b)。
77 /// </summary>
78 Equal = 13,
79 //
80 /// <summary>
81 /// 按位或逻辑 XOR 运算,如 C# 中的 (a ^ b) 或 Visual Basic 中的 (a Xor b)。
82 /// </summary>
83 ExclusiveOr = 14,
84 //
85 /// <summary>
86 /// “大于”比较,如 (a > b)。
87 /// </summary>
88 GreaterThan = 15,
89 //
90 /// <summary>
91 /// “大于或等于”比较,如 (a >= b)。
92 /// </summary>
93 GreaterThanOrEqual = 16,
94 //
95 /// <summary>
96 /// 调用委托或 lambda 表达式的运算,如 sampleDelegate.Invoke()。
97 /// </summary>
98 Invoke = 17,
99 //
100 /// <summary>
101 /// lambda 表达式,如 C# 中的 a => a + a 或 Visual Basic 中的 Function(a) a + a。
102 /// </summary>
103 Lambda = 18,
104 //
105 /// <summary>
106 /// 按位左移运算,如 (a << b)。
107 /// </summary>
108 LeftShift = 19,
109 //
110 /// <summary>
111 /// “小于”比较,如 (a < b)。
112 /// </summary>
113 LessThan = 20,
114 //
115 /// <summary>
116 /// “小于或等于”比较,如 (a <= b)。
117 /// </summary>
118 LessThanOrEqual = 21,
119 //
120 /// <summary>
121 /// 创建新的 System.Collections.IEnumerable 对象并从元素列表中初始化该对象的运算,如 C# 中的 new List<SampleType>(){
122 /// a, b, c } 或 Visual Basic 中的 Dim sampleList = { a, b, c }。
123 /// </summary>
124 ListInit = 22,
125 //
126 /// <summary>
127 /// 从字段或属性进行读取的运算,如 obj.SampleProperty。
128 /// </summary>
129 MemberAccess = 23,
130 //
131 /// <summary>
132 /// 创建新的对象并初始化其一个或多个成员的运算,如 C# 中的 new Point { X = 1, Y = 2 } 或 Visual Basic 中的
133 /// New Point With {.X = 1, .Y = 2}。
134 /// </summary>
135 MemberInit = 24,
136 //
137 /// <summary>
138 /// 算术余数运算,如 C# 中的 (a % b) 或 Visual Basic 中的 (a Mod b)。
139 /// </summary>
140 Modulo = 25,
141 //
142 /// <summary>
143 /// 乘法运算,如 (a * b),针对数值操作数,不进行溢出检查。
144 /// </summary>
145 Multiply = 26,
146 //
147 /// <summary>
148 /// 乘法运算,如 (a * b),针对数值操作数,进行溢出检查。
149 /// </summary>
150 MultiplyChecked = 27,
151 //
152 /// <summary>
153 /// 算术求反运算,如 (-a)。 不应就地修改 a 对象。
154 /// </summary>
155 Negate = 28,
156 //
157 /// <summary>
158 /// 一元加法运算,如 (+a)。 预定义的一元加法运算的结果是操作数的值,但用户定义的实现可以产生特殊结果。
159 /// </summary>
160 UnaryPlus = 29,
161 //
162 /// <summary>
163 /// 算术求反运算,如 (-a),进行溢出检查。 不应就地修改 a 对象。
164 /// </summary>
165 NegateChecked = 30,
166 //
167 /// <summary>
168 /// 调用构造函数创建新对象的运算,如 new SampleType()。
169 /// </summary>
170 New = 31,
171 //
172 /// <summary>
173 /// 创建新的一维数组并从元素列表中初始化该数组的运算,如 C# 中的 new SampleType[]{a, b, c} 或 Visual Basic
174 /// 中的 New SampleType(){a, b, c}。
175 /// </summary>
176 NewArrayInit = 32,
177 //
178 /// <summary>
179 /// 创建新数组(其中每个维度的界限均已指定)的运算,如 C# 中的 new SampleType[dim1, dim2] 或 Visual Basic
180 /// 中的 New SampleType(dim1, dim2)。
181 /// </summary>
182 NewArrayBounds = 33,
183 //
184 /// <summary>
185 /// 按位求补运算或逻辑求反运算。 在 C# 中,它与整型的 (~a) 和布尔值的 (!a) 等效。 在 Visual Basic 中,它与 (Not
186 /// a) 等效。 不应就地修改 a 对象。
187 /// </summary>
188 Not = 34,
189 //
190 /// <summary>
191 /// 不相等比较,如 C# 中的 (a != b) 或 Visual Basic 中的 (a <> b)。
192 /// </summary>
193 NotEqual = 35,
194 //
195 /// <summary>
196 /// 按位或逻辑 OR 运算,如 C# 中的 (a | b) 或 Visual Basic 中的 (a Or b)。
197 /// </summary>
198 Or = 36,
199 //
200 /// <summary>
201 /// 短路条件 OR 运算,如 C# 中的 (a || b) 或 Visual Basic 中的 (a OrElse b)。
202 /// </summary>
203 OrElse = 37,
204 //
205 /// <summary>
206 /// 对在表达式上下文中定义的参数或变量的引用。 有关更多信息,请参见 System.Linq.Expressions.ParameterExpression。
207 /// </summary>
208 Parameter = 38,
209 //
210 /// <summary>
211 /// 对某个数字进行幂运算的数学运算,如 Visual Basic 中的 (a ^ b)。
212 /// </summary>
213 Power = 39,
214 //
215 /// <summary>
216 /// 具有类型为 System.Linq.Expressions.Expression 的常量值的表达式。 System.Linq.Expressions.ExpressionType.Quote
217 /// 节点可包含对参数的引用,这些参数在该节点表示的表达式的上下文中定义。
218 /// </summary>
219 Quote = 40,
220 //
221 /// <summary>
222 /// 按位右移运算,如 (a >> b)。
223 /// </summary>
224 RightShift = 41,
225 //
226 /// <summary>
227 /// 减法运算,如 (a - b),针对数值操作数,不进行溢出检查。
228 /// </summary>
229 Subtract = 42,
230 //
231 /// <summary>
232 /// 算术减法运算,如 (a - b),针对数值操作数,进行溢出检查。
233 /// </summary>
234 SubtractChecked = 43,
235 //
236 /// <summary>
237 /// 显式引用或装箱转换,其中如果转换失败则提供 null,如 C# 中的 (obj as SampleType) 或 Visual Basic 中的
238 /// TryCast(obj, SampleType)。
239 /// </summary>
240 TypeAs = 44,
241 //
242 /// <summary>
243 /// 类型测试,如 C# 中的 obj is SampleType 或 Visual Basic 中的 TypeOf obj is SampleType。
244 /// </summary>
245 TypeIs = 45,
246 //
247 /// <summary>
248 /// 赋值运算,如 (a = b)。
249 /// </summary>
250 Assign = 46,
251 //
252 /// <summary>
253 /// 表达式块。
254 /// </summary>
255 Block = 47,
256 //
257 /// <summary>
258 /// 调试信息。
259 /// </summary>
260 DebugInfo = 48,
261 //
262 /// <summary>
263 /// 一元递减运算,如 C# 和 Visual Basic 中的 (a - 1)。 不应就地修改 a 对象。
264 /// </summary>
265 Decrement = 49,
266 //
267 /// <summary>
268 /// 动态操作。
269 /// </summary>
270 Dynamic = 50,
271 //
272 /// <summary>
273 /// 默认值。
274 /// </summary>
275 Default = 51,
276 //
277 /// <summary>
278 /// 扩展表达式。
279 /// </summary>
280 Extension = 52,
281 //
282 /// <summary>
283 /// “跳转”表达式,如 C# 中的 goto Label 或 Visual Basic 中的 GoTo Label。
284 /// </summary>
285 Goto = 53,
286 //
287 /// <summary>
288 /// 一元递增运算,如 C# 和 Visual Basic 中的 (a + 1)。 不应就地修改 a 对象。
289 /// </summary>
290 Increment = 54,
291 //
292 /// <summary>
293 /// 索引运算或访问使用参数的属性的运算。
294 /// </summary>
295 Index = 55,
296 //
297 /// <summary>
298 /// 标签。
299 /// </summary>
300 Label = 56,
301 //
302 /// <summary>
303 /// 运行时变量的列表。 有关更多信息,请参见 System.Linq.Expressions.RuntimeVariablesExpression。
304 /// </summary>
305 RuntimeVariables = 57,
306 //
307 /// <summary>
308 /// 循环,如 for 或 while。
309 /// </summary>
310 Loop = 58,
311 //
312 /// <summary>
313 /// 多分支选择运算,如 C# 中的 switch 或 Visual Basic 中的 Select Case。
314 /// </summary>
315 Switch = 59,
316 //
317 /// <summary>
318 /// 引发异常的运算,如 throw new Exception()。
319 /// </summary>
320 Throw = 60,
321 //
322 /// <summary>
323 /// try-catch 表达式。
324 /// </summary>
325 Try = 61,
326 //
327 /// <summary>
328 /// 取消装箱值类型运算,如 MSIL 中的 unbox 和 unbox.any 指令。
329 /// </summary>
330 Unbox = 62,
331 //
332 /// <summary>
333 /// 加法复合赋值运算,如 (a += b),针对数值操作数,不进行溢出检查。
334 /// </summary>
335 AddAssign = 63,
336 //
337 /// <summary>
338 /// 按位或逻辑 AND 复合赋值运算,如 C# 中的 (a &= b)。
339 /// </summary>
340 AndAssign = 64,
341 //
342 /// <summary>
343 /// 除法复合赋值运算,如 (a /= b),针对数值操作数。
344 /// </summary>
345 DivideAssign = 65,
346 //
347 /// <summary>
348 /// 按位或逻辑 XOR 复合赋值运算,如 C# 中的 (a ^= b)。
349 /// </summary>
350 ExclusiveOrAssign = 66,
351 //
352 /// <summary>
353 /// 按位左移复合赋值运算,如 (a <<= b)。
354 /// </summary>
355 LeftShiftAssign = 67,
356 //
357 /// <summary>
358 /// 算术余数复合赋值运算,如 C# 中的 (a %= b)。
359 /// </summary>
360 ModuloAssign = 68,
361 //
362 /// <summary>
363 /// 乘法复合赋值运算,如 (a *= b),针对数值操作数,不进行溢出检查。
364 /// </summary>
365 MultiplyAssign = 69,
366 //
367 /// <summary>
368 /// 按位或逻辑 OR 复合赋值运算,如 C# 中的 (a |= b)。
369 /// </summary>
370 OrAssign = 70,
371 //
372 /// <summary>
373 /// 对某个数字进行幂运算的复合赋值运算,如 Visual Basic 中的 (a ^= b)。
374 /// </summary>
375 PowerAssign = 71,
376 //
377 /// <summary>
378 /// 按位右移复合赋值运算,如 (a >>= b)。
379 /// </summary>
380 RightShiftAssign = 72,
381 //
382 /// <summary>
383 /// 减法复合赋值运算,如 (a -= b),针对数值操作数,不进行溢出检查。
384 /// </summary>
385 SubtractAssign = 73,
386 //
387 /// <summary>
388 /// 加法复合赋值运算,如 (a += b),针对数值操作数,进行溢出检查。
389 /// </summary>
390 AddAssignChecked = 74,
391 //
392 /// <summary>
393 /// 乘法复合赋值运算,如 (a *= b),针对数值操作数,进行溢出检查。
394 /// </summary>
395 MultiplyAssignChecked = 75,
396 //
397 /// <summary>
398 /// 减法复合赋值运算,如 (a -= b),针对数值操作数,进行溢出检查。
399 /// </summary>
400 SubtractAssignChecked = 76,
401 //
402 /// <summary>
403 /// 一元前缀递增,如 (++a)。 应就地修改 a 对象。
404 /// </summary>
405 PreIncrementAssign = 77,
406 //
407 /// <summary>
408 /// 一元前缀递减,如 (--a)。 应就地修改 a 对象。
409 /// </summary>
410 PreDecrementAssign = 78,
411 //
412 /// <summary>
413 /// 一元后缀递增,如 (a++)。 应就地修改 a 对象。
414 /// </summary>
415 PostIncrementAssign = 79,
416 //
417 /// <summary>
418 /// 一元后缀递减,如 (a--)。 应就地修改 a 对象。
419 /// </summary>
420 PostDecrementAssign = 80,
421 //
422 /// <summary>
423 /// 确切类型测试。
424 /// </summary>
425 TypeEqual = 81,
426 //
427 /// <summary>
428 /// 二进制反码运算,如 C# 中的 (~a)。
429 /// </summary>
430 OnesComplement = 82,
431 //
432 /// <summary>
433 /// true 条件值。
434 /// </summary>
435 IsTrue = 83,
436 //
437 /// <summary>
438 /// false 条件值。
439 /// </summary>
440 IsFalse = 84,
441 }
442 }


ExpressionType

 


在查询条件方面,要做的就是解析其中一些类型的表达式。


常用表达式举例



下面列举出常用的查询语句。


 



 1             System.Linq.Expressions.Expression<Func<Cat, bool>> pre = null;

2
3 pre = x => x.Price == 10;
4 pre = x => x.Price < 10;
5 pre = x => x.Price > 10;
6 pre = x => x.Price < 10 || x.Price > 20;
7 pre = x => 10 < x.Price && x.Price < 20;
8 pre = x => x.KittyName.Contains("2");
9 pre = x => x.KittyName.StartsWith("kitty");
10 pre = x => x.KittyName.EndsWith("2");


 


数据库文件的逻辑结构


块(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接口。



 1     /// <summary>

2 /// 存储到数据库文件的一块内容。
3 /// </summary>
4 [Serializable]
5 public abstract class Block : ISerializable
6 {
7
8 #if DEBUG
9
10 /// <summary>
11 /// 创建新<see cref="Block"/>时应设置其<see cref="Block.blockID"/>为计数器,并增长此计数器值。
12 /// </summary>
13 internal static long IDCounter = 0;
14
15 /// <summary>
16 /// 用于给此块标记一个编号,仅为便于调试之用。
17 /// </summary>
18 public long blockID;
19 #endif
20
21 /// <summary>
22 /// 此对象自身在数据库文件中的位置。为0时说明尚未指定位置。只有<see cref="DBHeaderBlock"/>的位置才应该为0。
23 /// <para>请注意在读写时设定此值。</para>
24 /// </summary>
25 public long ThisPos { get; set; }
26
27 /// <summary>
28 /// 存储到数据库文件的一块内容。
29 /// </summary>
30 public Block()
31 {
32 #if DEBUG
33 this.blockID = IDCounter++;
34 #endif
35 BlockCache.AddFloatingBlock(this);
36 }
37
38 #if DEBUG
39 const string strBlockID = "";
40 #endif
41
42 #region ISerializable 成员
43
44 /// <summary>
45 /// 序列化时系统会调用此方法。
46 /// </summary>
47 /// <param name="info"></param>
48 /// <param name="context"></param>
49 public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
50 {
51 #if DEBUG
52 info.AddValue(strBlockID, this.blockID);
53 #endif
54 }
55
56 #endregion
57
58 /// <summary>
59 /// BinaryFormatter会通过调用此方法来反序列化此块。
60 /// </summary>
61 /// <param name="info"></param>
62 /// <param name="context"></param>
63 protected Block(SerializationInfo info, StreamingContext context)
64 {
65 #if DEBUG
66 this.blockID = info.GetInt64(strBlockID);
67 #endif
68 }
69
70 /// <summary>
71 /// 显示此块的信息,便于调试。
72 /// </summary>
73 /// <returns></returns>
74 public override string ToString()
75 {
76 #if DEBUG
77 return string.Format("{0}: ID:{1}, Pos: {2}", this.GetType().Name, this.blockID, this.ThisPos);
78 #else
79 return string.Format("{0}: Pos: {1}", this.GetType().Name, this.ThisPos);
80 #endif
81 }
82
83 /// <summary>
84 /// 安排所有文件指针。如果全部安排完毕,返回true,否则返回false。
85 /// </summary>
86 /// <returns></returns>
87 public abstract bool ArrangePos();
88 }


 


 


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的字段/属性值都设置完毕。



 1             // 给所有的块安排数据库文件中的位置。

2 List<Block> arrangedBlocks = new List<Block>();
3 while (arrangedBlocks.Count < this.blockList.Count)
4 {
5 for (int i = this.blockList.Count - 1; i >= 0; i--)// 后加入列表的先处理。
6 {
7 Block block = this.blockList[i];
8 if (arrangedBlocks.Contains(block)) { continue; }
9 bool done = block.ArrangePos();
10 if (done)
11 {
12 if (block.ThisPos == 0)
13 {
14 byte[] bytes = block.ToBytes();
15 if (bytes.Length > Consts.maxAvailableSpaceInPage)
16 { throw new Exception("Block size is toooo large!"); }
17 AllocPageTypes pageType = block.BelongedPageType();
18 AllocatedSpace space = this.fileDBContext.Alloc(bytes.LongLength, pageType);
19 block.ThisPos = space.position;
20 }
21
22 arrangedBlocks.Add(block);
23 }
24 }
25 }


 


我们要为不同类型的块执行各自的字段/属性值的设置方法,通过继承Block基类的abstract bool ArrangePos();来实现。实际上,只要添加到blockList里的顺序得当,只需一次遍历即可完成所有的自动/属性值的设置。 


DBHeaderBlock


此类型的字段/属性值不依赖其它任何Block,永远都是实时分配完成的。



1         internal override bool ArrangePos()

2 {
3 return true;// 此类型比较特殊,应该在更新时立即指定各项文件指针。
4 }


 


TableBlock


作为头结点的那个TableBlock不含索引,因此其字段/属性值不需设置。其它TableBlock则需要保存索引头结点的位置。


作为链表的最后一个结点的那个TableBlock的字段/属性值不依赖其它TableBlock的位置,其它TableBLock则需要其下一个TableBlock的位置。这一规则对每个文件链表都适用。



 1         public override bool ArrangePos()

2 {
3 bool allArranged = true;
4
5 if (this.IndexBlockHead != null)// 如果IndexBlockHead == null,则说明此块为TableBlock的头结点。头结点是不需要持有索引块的。
6 {
7 if (this.IndexBlockHead.ThisPos != 0)
8 { this.IndexBlockHeadPos = this.IndexBlockHead.ThisPos; }
9 else
10 { allArranged = false; }
11 }
12
13 if (this.NextObj != null)
14 {
15 if (this.NextObj.ThisPos != 0)
16 { this.NextPos = this.NextObj.ThisPos; }
17 else
18 { allArranged = false; }
19 }
20
21 return allArranged;
22 }


 


IndexBlock


IndexBlock也是文件链表的一个结点,其字段/属性值的设置方式与TableBlock相似。



 1         public override bool ArrangePos()

2 {
3 bool allArranged = true;
4
5 if (this.SkipListHeadNodes != null)// 如果这里的SkipListHeadNodes == null,则说明此索引块是索引链表里的头结点。头结点是不需要SkipListHeadNodes有数据的。
6 {
7 int length = this.SkipListHeadNodes.Length;
8 if (length == 0)
9 { throw new Exception("SKip List's head nodes has 0 element!"); }
10 long pos = this.SkipListHeadNodes[length - 1].ThisPos;
11 if (pos != 0)
12 { this.SkipListHeadNodePos = pos; }
13 else
14 { allArranged = false; }
15 }
16
17 if (this.SkipListTailNode != null)// 如果这里的SkipListTailNodes == null,则说明此索引块是索引链表里的头结点。头结点是不需要SkipListTailNodes有数据的。
18 {
19 long pos = this.SkipListTailNode.ThisPos;
20 if (pos != 0)
21 { this.SkipListTailNodePos = pos; }
22 else
23 { allArranged = false; }
24 }
25
26 if (this.NextObj != null)
27 {
28 if (this.NextObj.ThisPos != 0)
29 { this.NextPos = this.NextObj.ThisPos; }
30 else
31 { allArranged = false; }
32 }
33
34 return allArranged;
35 }


 


SkipListNodeBlock


SkipListNodeBlock是扩展了的文件链表,关于指向下一结点的指针的处理与前面类似。作为头结点的SkipListNodeBlock的Key和Value都是null,是不依赖其它Block的。非头结点的SkipListNodeBlock的Key和Value则依赖保存着序列化后的Key和Value的DataBlock。



 1         public override bool ArrangePos()

2 {
3 bool allArranged = true;
4
5 if (this.Key != null)
6 {
7 if (this.Key.ThisPos != 0)
8 { this.KeyPos = this.Key.ThisPos; }
9 else
10 { allArranged = false; }
11 }
12
13 if (this.Value != null)
14 {
15 if (this.Value[0].ThisPos != 0)
16 { this.ValuePos = this.Value[0].ThisPos; }
17 else
18 { allArranged = false; }
19 }
20
21 if (this.DownObj != null)// 此结点不是最下方的结点。
22 {
23 if (this.DownObj.ThisPos != 0)
24 { this.DownPos = this.DownObj.ThisPos; }
25 else
26 { allArranged = false; }
27 }
28
29 if (this.RightObj != null)// 此结点不是最右方的结点。
30 {
31 if (this.RightObj.ThisPos != 0)
32 { this.RightPos = this.RightObj.ThisPos; }
33 else
34 { allArranged = false; }
35 }
36
37 return allArranged;
38 }


 


DataBlock


DataBlock就是一个很单纯的文件链表结点。



 1         public override bool ArrangePos()

2 {
3 bool allArranged = true;
4
5 if (NextObj != null)
6 {
7 if (NextObj.ThisPos != 0)
8 { this.NextPos = NextObj.ThisPos; }
9 else
10 { allArranged = false; }
11 }
12
13 return allArranged;
14 }


 


PageHeaderBlock


此类型只在创建新页和申请已有页空间时出现,不会参与上面各类Block的位置分配,而是在创建时就为其安排好NextPagePos等属性。



1         internal override bool ArrangePos()

2 {
3 return true;// 此类型比较特殊,应该在创建时就为其安排好NextPagePos等属性。
4 }


 


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。还是那句话,欢迎您指出我的代码有哪些不足,我应该学习补充哪些知识,但请不要无意义地谩骂和人身攻击。


 


PS:我国大多数县的人口为几万到几十万。目前,县里各种政府部门急需实现信息化网络化办公办事,但他们一般用不起那种月薪上万的开发者和高端软件公司。我注意到,一个县级政府部门日常应对的人群数量就是万人左右,甚至常常是千人左右。所以他们不需要太高端复杂的系统设计,用支持万人级别的数据库就可以了。另一方面,初级开发者也不能充分利用那些看似高端复杂的数据库的优势。做个小型系统而已,还是简单一点好。


所以我就想做这样一个小型文件数据库,我相信这会帮助很多人。能以己所学惠及大众,才是我们的价值所在。