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

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

  • 相关阅读:
    mixin混合
    python类内部调用自己的成员函数必须加self
    合并/取消合并单元格
    pandas 显示所有的行和列
    pandas 利用openpyxl设置表格样式
    Nowcoder9983G.糖果(并查集)
    Nowcoder9983F.匹配串(思维)
    Nowcoder9983E.买礼物(链表+线段树)
    Nowcoder9983C.重力追击(暴力+少用sqrt)
    Nowcoder9983B.内卷(双指针)
  • 原文地址:https://www.cnblogs.com/bitzhuwei/p/SharpFileDB.html
Copyright © 2020-2023  润新知