小型文件数据库 (a file database for small apps) SharpFileDB

小型文件数据库 (a file database for small apps) SharpFileDB

For english version of this article, please click here.

我并不擅长数据库,如有不当之处,请多多指教。

本文参考了(http://www.cnblogs.com/gaochundong/archive/2013/04/24/csharp_file_database.html),在此表示感谢!

目标(Goal)

我决定做一个以支持小型应用(万人级别)为目标的数据库。

既然是小型的数据库,那么最好不要依赖其它驱动、工具包,免得拖泥带水难以实施。

完全用C#编写成DLL,易学易用。

支持CRUD(增加(Create)、读取(Retrieve)、更新(Update)和删除(Delete))。

不使用SQL,客观原因我不擅长SQL,主观原因我不喜欢SQL,情景原因没有必要。

直接用文本文件或二进制文件存储数据。开发时用文本文件,便于调试;发布时用二进制文件,比较安全。

简单来说,就是纯C#、小型、无SQL。此类库就命名为SharpFileDB

为了便于共同开发,我把这个项目放到Github上,并且所有类库代码的注释都是中英文双语的。中文便于理解,英文便于今后国际化。也许我想的太多了。

设计草图(sketch)

使用场景(User Scene)

SharpFileDB库的典型使用场景如下。

 1                 // common cases to use SharpFileDB.
 2                 FileDBContext db = new FileDBContext();
 3 
 4                 Cat cat = new Cat();
 5                 cat.Name = "xiao xiao bai";
 6                 db.Create(cat);
 7 
 8                 Predicate<Cat> pre = new Predicate<Cat>(x => x.Name == "xiao xiao bai");
 9                 IList<Cat> cats = db.Retrieve(pre);
10 
11                 cat.Name = "xiao bai";
12                 db.Update(cat);
13 
14                 db.Delete(cat);

这个场景里包含了创建数据库和使用CRUD操作的情形。

我们就从这个使用场景开始设计出第一版最简单的一个文件数据库。

核心概念(Core Concepts)

如下图所示,数据库有三个核心的东西:数据库上下文,就是数据库本身,能够执行CRUD操作;表,在这里是一个个文件,用一个FileObject类型表示一个表;持久化工具,实现CRUD操作,把信息存储到数据库中。

 

表vs类型(Table vs Type)

为方便叙述,下面我们以Cat为例进行说明。

 1     /// <summary>
 2     /// demo file object
 3     /// </summary>
 4     public class Cat : FileObject
 5     {
 6         public string Name { get; set; }
 7         public int Legs { get; set; }
 8 
 9         public override string ToString()
10         {
11             return string.Format("{0}, Name: {1}, Legs: {2}", base.ToString(), Name, Legs);
12         }
13     }

Cat这个类型就等价于关系数据库里的一个Table。

Cat的一个实例,就等价于关系数据库的Table里的一条记录。

以后我们把这样的类型称为表类型

全局唯一的主键(global unique main key)

类似关系数据库的主键,我们需要用全局唯一的Id来区分每个对象。每个表类型的实例都需要这样一个Id,那么我们就用一个abstract基类做这件事。

 1     /// <summary>
 2     /// 可在文件数据库中使用CRUD操作的所有类型的基类。
 3     /// Base class for all classed that can use CRUD in SharpFileDB.
 4     /// </summary>
 5     [Serializable]
 6     public abstract class FileObject
 7     {
 8         /// <summary>
 9         /// 主键.
10         /// main key.
11         /// </summary>
12         public Guid Id { get; set; }
13 
14         /// <summary>
15         /// 创建一个文件对象,并自动为其生成一个全局唯一的Id。
16         /// <para>Create a <see cref="FileObject"/> and generate a global unique id for it.</para>
17         /// </summary>
18         public FileObject()
19         {
20             this.Id = Guid.NewGuid();
21         }
22 
23         public override string ToString()
24         {
25             return string.Format("Id: {0}", this.Id);
26         }
27     }

 

数据库(FileDBContext)

一个数据库上下文负责各种类型的文件对象的CRUD操作。

  1     /// <summary>
  2 /// 文件数据库。
  3     /// Represents a file database.
  4     /// </summary>
  5     public class FileDBContext
  6     {
  7         #region Fields
  8 
  9         /// <summary>
 10         /// 文件数据库操作锁
 11         /// <para>database operation lock.</para>
 12         /// </summary>
 13         protected static readonly object operationLock = new object();
 14 
 15         /// <summary>
 16         /// 文件数据库
 17         /// <para>Represents a file database.</para>
 18         /// </summary>
 19         /// <param name="directory">数据库文件所在目录<para>Directory for all files of database.</para></param>
 20         public FileDBContext(string directory = null)
 21         {
 22             if (directory == null)
 23             {
 24                 this.Directory = Environment.CurrentDirectory;
 25             }
 26             else
 27             {
 28                 Directory = directory;
 29             }
 30         }
 31 
 32         #endregion
 33 
 34         public override string ToString()
 35         {
 36             return string.Format("@: {0}", Directory);
 37         }
 38 
 39         #region Properties
 40 
 41         /// <summary>
 42         /// 数据库文件所在目录
 43         /// <para>Directory of database files.</para>
 44         /// </summary>
 45         public virtual string Directory { get; protected set; }
 46 
 47         #endregion
 48 
 49 
 50         protected string Serialize(FileObject item)
 51         {
 52             using (StringWriterWithEncoding sw = new StringWriterWithEncoding(Encoding.UTF8))
 53             {
 54                 XmlSerializer serializer = new XmlSerializer(item.GetType());
 55                 serializer.Serialize(sw, item);
 56                 string serializedString = sw.ToString();
 57 
 58                 return serializedString;
 59             }
 60         }
 61 
 62         /// <summary>
 63         /// 将字符串反序列化成文档对象
 64         /// </summary>
 65         /// <typeparam name="TDocument">文档类型</typeparam>
 66         /// <param name="serializedFileObject">字符串</param>
 67         /// <returns>
 68         /// 文档对象
 69         /// </returns>
 70         protected TFileObject Deserialize<TFileObject>(string serializedFileObject)
 71             where TFileObject : FileObject
 72         {
 73             if (string.IsNullOrEmpty(serializedFileObject))
 74                 throw new ArgumentNullException("data");
 75 
 76             using (StringReader sr = new StringReader(serializedFileObject))
 77             {
 78                 XmlSerializer serializer = new XmlSerializer(typeof(TFileObject));
 79                 object deserializedObj = serializer.Deserialize(sr);
 80                 TFileObject fileObject = deserializedObj as TFileObject;
 81                 return fileObject;
 82             }
 83         }
 84 
 85         protected string GenerateFileFullPath(FileObject item)
 86         {
 87             string path = GenerateFilePath(item.GetType());
 88             string name = item.GenerateFileName();
 89             string fullname = Path.Combine(path, name);
 90             return fullname;
 91         }
 92 
 93         /// <summary>
 94         /// 生成文件路径
 95         /// </summary>
 96         /// <typeparam name="TDocument">文档类型</typeparam>
 97         /// <returns>文件路径</returns>
 98         protected string GenerateFilePath(Type type)
 99         {
100             string path = Path.Combine(this.Directory, type.Name);
101             return path;
102         }
103 
104         #region CRUD
105 
106         /// <summary>
107         /// 增加一个<see cref="FileObject"/>到数据库。这实际上创建了一个文件。
108         /// <para>Create a new <see cref="FileObject"/> into database. This operation will create a new file.</para>
109         /// </summary>
110         /// <param name="item"></param>
111         public virtual void Create(FileObject item)
112         {
113             string fileName = GenerateFileFullPath(item);
114             string output = Serialize(item);
115 
116             lock (operationLock)
117             {
118                 System.IO.FileInfo info = new System.IO.FileInfo(fileName);
119                 System.IO.Directory.CreateDirectory(info.Directory.FullName);
120                 System.IO.File.WriteAllText(fileName, output);
121             }
122         }
123 
124         /// <summary>
125         /// 检索符合给定条件的所有<paramref name="TFileObject"/>126         /// <para>Retrives all <paramref name="TFileObject"/> that satisfies the specified condition.</para>
127         /// </summary>
128         /// <typeparam name="TFileObject"></typeparam>
129         /// <param name="predicate">检索出的对象应满足的条件。<para>THe condition that should be satisfied by retrived object.</para></param>
130         /// <returns></returns>
131         public virtual IList<TFileObject> Retrieve<TFileObject>(Predicate<TFileObject> predicate)
132             where TFileObject : FileObject
133         {
134             IList<TFileObject> result = new List<TFileObject>();
135             if (predicate != null)
136             {
137                 string path = GenerateFilePath(typeof(TFileObject));
138                 string[] files = System.IO.Directory.GetFiles(path, "*.xml", SearchOption.AllDirectories);
139                 foreach (var item in files)
140                 {
141                     string fileContent = File.ReadAllText(item);
142                     TFileObject deserializedFileObject = Deserialize<TFileObject>(fileContent);
143                     if (predicate(deserializedFileObject))
144                     {
145                         result.Add(deserializedFileObject);
146                     }
147                 }
148             }
149 
150             return result;
151         }
152 
153         /// <summary>
154         /// 更新给定的对象。
155         /// <para>Update specified <paramref name="FileObject"/>.</para>
156         /// </summary>
157         /// <param name="item">要被更新的对象。<para>The object to be updated.</para></param>
158         public virtual void Update(FileObject item)
159         {
160             string fileName = GenerateFileFullPath(item);
161             string output = Serialize(item);
162 
163             lock (operationLock)
164             {
165                 System.IO.FileInfo info = new System.IO.FileInfo(fileName);
166                 System.IO.Directory.CreateDirectory(info.Directory.FullName);
167                 System.IO.File.WriteAllText(fileName, output);
168             }
169         }
170 
171         /// <summary>
172         /// 删除指定的对象。
173         /// <para>Delete specified <paramref name="FileObject"/>.</para>
174         /// </summary>
175         /// <param name="item">要被删除的对象。<para>The object to be deleted.</para></param>
176         public virtual void Delete(FileObject item)
177         {
178             if (item == null)
179             {
180                 throw new ArgumentNullException(item.ToString());
181             }
182 
183             string filename = GenerateFileFullPath(item);
184             if (File.Exists(filename))
185             {
186                 lock (operationLock)
187                 {
188                     File.Delete(filename);
189                 }
190             }
191         }
192 
193         #endregion CRUD
194 
195     }
FileDBContext

 

文件存储方式(Way to store files)

在数据库目录下,SharpFileDB为每个表类型创建一个文件夹,在各自文件夹内存储每个对象。每个对象都占用一个XML文件。暂时用XML格式,因为是.NET内置的格式,省的再找外部序列化工具。XML文件名与其对应的对象Id相同。

 

 

下载(Download)

我已将源码放到(https://github.com/bitzhuwei/SharpFileDB/),欢迎试用、提建议或Fork此项目。

更新(Update)

2015-06-22

增加了序列化接口(IPersistence),使得FileDBContext可以选择序列化器。

增加了二进制序列化类型(BinaryPersistence)。

使用Convert.ToBase64String()和Convert.FromBase64String()实现Byte数组与string之间的转换。

1 //Image-->Byte[]-->String 
2  Byte[] bytes = File.ReadAllBytes(@"d:\a.gif"); 
3  MemoryStream ms = new MemoryStream(bty); 
4  String imgStr = Convert.ToBase64String(ms.ToArray());
5 
6 //String-->Byte[]-->Image 
7  byte[] imgBytes = Convert.FromBase64String(imgStr); 
8  Response.BinaryWrite(imgBytes.ToArray());  // 将一个二制字符串写入HTTP输出流

 

修改了接口IPersistence,让它直接进行内存数据与文件之间的转化。这样,即使序列化的结果是byte[]或其它类型,也可以直接保存到文件,不再需要先转化为string后再保存。

 1     /// <summary>
 2     /// 文件数据库使用此接口进行持久化相关的操作。
 3     /// <para>File database executes persistence operations via this interface.</para>
 4     /// </summary>
 5     public interface IPersistence
 6     {
 7         /// <summary>
 8         /// <see cref="FileObject"/>文件的扩展名。
 9         /// Extension name of <see cref="FileObject"/>'s file.
10         /// </summary>
11         string Extension { get; }
12 
13         /// <summary>
14         /// 将文件对象序列化为文件。
15         /// <para>Serialize the specified <paramref name="item"/> into <paramref name="fullname"/>.</para>
16         /// </summary>
17         /// <param name="item">要进行序列化的文件对象。<para>file object to be serialized.</para></param>
18         /// <param name="fullname">要保存到的文件的绝对路径。<para>file's fullname.</para></param>
19         /// <returns></returns>
20         void Serialize([Required] FileObject item, [Required] string fullname);
21 
22         /// <summary>
23         /// 将文件反序列化成文件对象。
24         /// <para>Deserialize the specified file to an instance of <paramref name="TFileObject"/>.</para>
25         /// </summary>
26         /// <typeparam name="TFileObject"></typeparam>
27         /// <param name="serializedFileObject"></param>
28         /// <returns></returns>
29         TFileObject Deserialize<TFileObject>([Required] string fullname) where TFileObject : FileObject;
30     }

 

使用接口ISerializable,让每个FileObject都自行处理自己的字段、属性的序列化和反序列化动作(保存、忽略等)。

 1     /// <summary>
 2     /// 可在文件数据库中使用CRUD操作的所有类型的基类。类似于关系数据库中的Table。
 3     /// Base class for all classed that can use CRUD in SharpFileDB. It's similar to the concept 'table' in relational database.
 4     /// </summary>
 5     [Serializable]
 6     public abstract class FileObject : ISerializable
 7     {
 8         /// <summary>
 9         /// 用以区分每个Table的每条记录。
10         /// This Id is used for diffrentiate instances of 'table's.
11         /// </summary>
12         public Guid Id { get; internal set; }
13 
14         /// <summary>
15         /// 创建一个文件对象,在用<code>FileDBContext.Create();</code>将此对象保存到数据库之前,此对象的Id为<code>Guid.Empty</code>16         /// <para>Create a <see cref="FileObject"/> whose Id is <code>Guid.Empty</code> until it's saved to database by <code>FileDBContext.Create();</code>.</para>
17         /// </summary>
18         public FileObject()
19         {
20         }
21 
22         /// <summary>
23         /// 生成文件名,此文件将用于存储此<see cref="FileObject"/>的内容。
24         /// Generate file name that will contain this instance's data of <see cref="FileObject"/>.
25         /// </summary>
26         /// <param name="extension">文件扩展名。<para>File's extension name.</para></param>
27         /// <returns></returns>
28         internal string GenerateFileName([Required] string extension)
29         {
30             string id = this.Id.ToString();
31 
32             string name = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", id, extension);
33 
34             return name;
35         }
36 
37         public override string ToString()
38         {
39             return string.Format("Id: {0}", this.Id);
40         }
41 
42         const string strGuid = "Guid";
43 
44         #region ISerializable 成员
45 
46         /// <summary>
47         /// This method will be invoked automatically when IFormatter.Serialize() is called.
48         /// <para>You must use <code>base(info, context);</code> in the derived class to feed <see cref="FileObject"/>'s fields and properties.</para>
49         /// <para>当使用IFormatter.Serialize()时会自动调用此方法。</para>
50         /// <para>继承此类型时,必须在子类型中用<code>base(info, context);</code>来填充<see cref="FileObject"/>自身的数据。</para>
51         /// </summary>
52         /// <param name="info"></param>
53         /// <param name="context"></param>
54         public virtual void GetObjectData([Required] SerializationInfo info, StreamingContext context)
55         {
56             info.AddValue(strGuid, this.Id.ToString());
57         }
58 
59         #endregion
60 
61         /// <summary>
62         /// This method will be invoked automatically when IFormatter.Serialize() is called.
63         /// <para>You must use <code>: base(info, context)</code> in the derived class to feed <see cref="FileObject"/>'s fields and properties.</para>
64         /// <para>当使用IFormatter.Serialize()时会自动调用此方法。</para>
65         /// <para>继承此类型时,必须在子类型中用<code>: base(info, context)</code>来填充<see cref="FileObject"/>自身的数据。</para>
66         /// </summary>
67         /// <param name="info"></param>
68         /// <param name="context"></param>
69         protected FileObject([Required] SerializationInfo info, StreamingContext context)
70         {
71             string str = (string)info.GetValue(strGuid, typeof(string));
72             this.Id = Guid.Parse(str);
73         }
74     }
FileObject相当于关系数据库中的Table

另外,FileObject在使用new FileObject();创建时不为其指定Guid,而在FileDBContext.Create(FileObject)时才进行指定。这样,在反序列化时就不必浪费时间去白白指定一个即将被替换的Guid了。这也更合乎情理:只有那些已经存储到数据库或立刻就要存储到数据库的FileObject才有必要拥有一个Guid。

 

用一个DefaultPersistence类型代替了BinaryPersistence和XmlPersistence。由于SoapFormatter和BinaryFormatter是近亲,而XmlSerializer跟他们是远亲;同时SoapFormatter和BinaryFormatter分别实现了文本文件序列化和二进制序列化,XmlSerializer就更不用出场了。因此现在不再使用XmlSerializer。

 1     /// <summary>
 2     ///<see cref="IFormatter"/>实现<see cref="IPersistence"/> 3     /// <para>Implement <see cref="IPersistence"/> using <see cref="IFormatter"/>.</para>
 4     /// </summary>
 5     public class DefaultPersistence : IPersistence
 6     {
 7         private System.Runtime.Serialization.IFormatter formatter;
 8 
 9         public DefaultPersistence(PersistenceFormat format = PersistenceFormat.Soap)
10         {
11             switch (format)
12             {
13                 case PersistenceFormat.Soap:
14                     this.formatter = new System.Runtime.Serialization.Formatters.Soap.SoapFormatter();
15                     this.Extension = "soap";
16                     break;
17                 case PersistenceFormat.Binary:
18                     this.formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
19                     this.Extension = "bin";
20                     break;
21                 default:
22                     throw new NotImplementedException();
23             }
24         }
25 
26         public enum PersistenceFormat
27         {
28             Soap,
29             Binary,
30         }
31 
32         #region IPersistence 成员
33 
34         private string extension;
35         public string Extension
36         {
37             get { return this.extension; }
38             private set { this.extension = value; }
39         }
40 
41         public void Serialize(FileObject item, string fullname)
42         {
43             if (item == null)
44             {
45                 throw new ArgumentNullException("item");
46             }
47 
48             if (string.IsNullOrEmpty(fullname))
49             {
50                 throw new ArgumentNullException("fullname");
51             }
52 
53             using (FileStream s = new FileStream(fullname, FileMode.Create, FileAccess.Write))
54             {
55                 formatter.Serialize(s, item);
56             }
57         }
58 
59         public TFileObject Deserialize<TFileObject>(string fullname) where TFileObject : FileObject
60         {
61             if(string.IsNullOrEmpty(fullname))
62             {
63                 throw new ArgumentNullException("fullname");
64             }
65 
66             TFileObject fileObject = null;
67 
68             using (FileStream s = new FileStream(fullname, FileMode.Open, FileAccess.Read))
69             {
70                 object obj = formatter.Deserialize(s);
71                 fileObject = obj as TFileObject;
72             }
73 
74             return fileObject;
75         }
76 
77         #endregion
78 
79     }
支持Soap和binary的持久化工具。

 

2015-06-23

把FileObject重命名为Document。追随LiteDB的命名。 

新增Demo项目MyNote,演示如何使用SharpFileDB。

 2015-06-24

经不完全测试,当写入同一文件夹内的文件数目超过百万时,下述序列化方式所需时间加倍。

1                     using (FileStream s = new FileStream(fullname, FileMode.Create, FileAccess.Write))
2                     {
3                         formatter.Serialize(s, string.Empty);
4                     }

继续测试中。

2015-06-25

根据上述试验和对事务、索引等的综合考虑,决定不再采用“一个数据库记录(Document)放到一个单独的文件里”这种方案。因此到目前为止的SharpFileDB作为初次尝试的版本,不再更新,今后将重新设计一套单文件数据库。

我把这个版本的项目源码放到这里。它超级简单,只有3个类,你不需懂SQL,只要会用C#就能使用。还附有一个Demo:便条(MyNote),你可以参考。

如果你的应用程序所需保存的数据库记录在几万条的规模,用这个是没问题的。

点此下载源码SharpFileDB.Version0.1.MultiFiles

Document这个类代表一条数据库记录。

 1     [Serializable]
 2     public abstract class Document : ISerializable
 3     {
 4         public Guid Id { get; internal set; }
 5 
 6         public Document()
 7         {
 8         }
 9 
10         internal string GenerateFileName(string extension)
11         {
12             string id = this.Id.ToString();
13 
14             string name = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", id, extension);
15 
16             return name;
17         }
18 
19         public override string ToString()
20         {
21             return string.Format("Id: {0}", this.Id);
22         }
23 
24         /// <summary>
25         /// 使用的字符越少,序列化时占用的字节就越少。一个字符都不用最好。
26         /// <para>Using less chars means less bytes after serialization. And "" is allowed.</para>
27         /// </summary>
28         const string strGuid = "";
29 
30         #region ISerializable 成员
31 
32         public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
33         {
34             string id = this.Id.ToString();
35             info.AddValue(strGuid, id);
36         }
37 
38         #endregion
39 
40         protected Document(SerializationInfo info, StreamingContext context)
41         {
42             string str = info.GetString(strGuid);
43             this.Id = Guid.Parse(str);
44         }
45     }

这个类就是被设计了用做基类供使用者继承的;另外也需要对其进行序列化,所以我希望 const string strGuid = "";  有两个特点:

1最短(减少序列化后的字节数)很明显,单个字符最短了。一个字符都不用那是不行的。

2最不易被别人重复使用(比如我要是用 const string strGuid = "a"; 什么的,别人在子类型中也出现的概率就比"~"大)

经测试发现,BinaryFormatter可以接受 const string strGuid = ""; 所以改用这个设定。

2015-07-06

根据现有代码和从LiteDB得到的启发,决定重新设计编写一个单文件数据库。目前的代码全部作废,不过保留起来备用,因为其中一些最基础的功能还是会用到的。

 

待完成的工作

必须支持事务ACID。

必须使用索引。参考LiteDB的skip list方式。

必须分页,每页4096bytes。这是读写磁盘文件的最小单位。充分利用之,可以提升I/O效率。(https://github.com/mbdavid/LiteDB/wiki/How-LiteDB-Works

 

 

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

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

posted @ 2015-06-22 02:04  BIT祝威  阅读(8946)  评论(11编辑  收藏  举报