小型文件数据库 (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 }
文件存储方式(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在使用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 }
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:我国大多数县的人口为几万到几十万。目前,县里各种政府部门急需实现信息化网络化办公办事,但他们一般用不起那种月薪上万的开发者和高端软件公司。我注意到,一个县级政府部门日常应对的人群数量就是万人左右,甚至常常是千人左右。所以他们不需要太高端复杂的系统设计,用支持万人级别的数据库就可以了。另一方面,初级开发者也不能充分利用那些看似高端复杂的数据库的优势。做个小型系统而已,还是简单一点好。
所以我就想做这样一个小型文件数据库,我相信这会帮助很多人。能以己所学惠及大众,才是我们的价值所在。
微信扫码,自愿捐赠。天涯同道,共谱新篇。
微信捐赠不显示捐赠者个人信息,如需要,请注明联系方式。 |