1. 程式人生 > 實用技巧 >超輕量級資料庫 iboxDB 以及其使用

超輕量級資料庫 iboxDB 以及其使用

  之前用的 sqlite3 作為本地資料庫, 可是它不能作為記憶體資料庫, 是基於檔案的, 在某些情況下沒有讀寫許可權就直接掛壁了, 比如 WebGL 中會報錯 dlopen(), 然後給了一個連結, 看過去太複雜了沒有懂, 或者安卓裡面 StreamingAssets 是壓縮包檔案, 也是沒法直接使用的......

  而且sqlite3 用起來很麻煩, dll 需要同時引用 Mono.Data 和 System.Data, 在Unity2017中需要手動扔一個 System.Data 進去, 要不然缺失引用, 而在 Unity2019中又不能扔進去, 會編譯衝突......

  然後找到這個, 很簡單一個dll完事 :

  

  它的讀取可以通過 path, byte[], Stream 等來實現, 能夠實現很多種需求了.

  不過有點奇葩的是它的檔案命名方式, 比如我想要建立一個 abc.db 檔案, 這是不行的, 只能傳給它數字, 然後它自己生成 db{N}.box 這樣的 db 檔案, 或者傳給它一個資料夾路徑, 它會自動生成資料夾下 db1.box 檔案, 實在夠奇怪的, 不過生成出來的檔案, 可以通過改名, 然後讀取 bytes 的方式讀取......

  反正是很神奇的腦回路, 我搞了半天才明白什麼回事, 它也沒有文件, 導致後面出現了一系列事故.

  先來說說怎樣生成資料庫, 比如從 Excel 或是啥來源的資料, 要把它生成資料庫的流程很簡單, 就是先獲取 Table 的 Key, 然後每行作為對應的資料錄入資料庫就行了, 可是插入資料在 iboxDB 裡面是個很奇葩的操作 :

  AutoBox 是資料操作的入口, 它的插入只有泛型的 Insert<V> 來實現, 它的 API 設計是基於已存在的型別的, 比如一個數據庫你要儲存一個類 :

    public class Record
    {
        public string Id;
        public string Name;
        public string age;
    }

  對於已經存在的型別, 它就很簡單 :

    AutoBox autoBox = ......
    var rec = new Record { Id = "aa", Name = "
Andy" }; autoBox.Insert<Record>("hahaha", rec);

  可是對於一個剛從 Excel 來的資料, 我們是沒有型別的, 那麼怎樣才能建立一個型別給它?

  這時候只能使用 Emit 了, 沒有型別就建立型別, 然後它沒有非泛型方法, 建立型別之後還需要從 Type 獲取泛型 Insert<V> 方法, 非常麻煩 :

        /// <summary>
        /// Generate IL code for no exsists type
        /// </summary>
        /// <param name="typeName"></param>
        /// <param name="vars"></param>
        /// <returns></returns>
        public static System.Type DataBaseRawTypeILGenerator(string typeName, params string[] vars)
        {
            // 構建程式集
            var asmName = new AssemblyName("DataBaseRawType");
            var asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndSave);
            // 構建模組
            ModuleBuilder mdlBldr = asmBuilder.DefineDynamicModule(asmName.Name, asmName.Name + ".dll");
            // 構建類
            var typeBldr = mdlBldr.DefineType(typeName, TypeAttributes.Public);
            // 建立field
            if(vars != null && vars.Length > 0)
            {
                foreach(var variance in vars)
                {
                    FieldBuilder fbNumber = typeBldr.DefineField(variance, typeof(string), FieldAttributes.Public);
                }
            }
            var t = typeBldr.CreateType();
            return t;
        }

  通過建立型別, 傳入 { "Id", "Name", "age" }可以創建出一個跟 Record 一樣的擁有這些變數的型別, 然後需要根據它獲取 AutoBox 例項的 Insert<V> 泛型方法 :

    public static MethodInfo GetGenericFunction(System.Type type, string genericFuncName, Type[] genericTypes, object[] paramaters, bool isStatic)
    {
        var flags = BindingFlags.Public | BindingFlags.NonPublic | (isStatic ? BindingFlags.Static : BindingFlags.Instance) | BindingFlags.InvokeMethod;
        var methods = type.GetMethods(flags);
        foreach(var method in methods)
        {
            if(method.IsGenericMethod && string.Equals(method.Name, genericFuncName, StringComparison.Ordinal))
            {
                var arguments = method.GetGenericArguments();   // 檢查泛型類的數量是否對的上
                if(arguments != null && arguments.Length == genericTypes.Length)
                {
                    // 檢查傳入引數型別是否對的上, 如果考慮到可變引數, default value引數, 可空結構體引數等, 會很複雜
                    if(MethodParametersTypeEquals(method, paramaters))
                    {
                        var genericMethod = method.MakeGenericMethod(genericTypes);
                        if(genericMethod != null)
                        {
                            return genericMethod;
                        }
                    }
                }
            }
        }
        return null;
    }
    // 簡單的對比一下, 實際使用要考慮到可變引數( params object[] ), default value引數( bool isStatic = false ), 可空結構體引數( int? a = null )等
    public static bool MethodParametersTypeEquals(MethodInfo method, object[] parameters)
    {
        var mehotdParamters = method.GetParameters();
        int len_l = mehotdParamters != null ? mehotdParamters.Length : 0;
        int len_r = parameters != null ? parameters.Length : 0;
        return len_l == len_r;
    }

  這兩個大招還是之前測試 Lua 泛型的時候搞的, 沒想到會用到這裡來, 然後就是依靠

    System.Activator.CreateInstance(type);

  來建立例項儲存資料了, 它的設計基於簡單易用, 可是在這裡就變得很複雜, 好在有 Emit 大法......

  然後就能走通流程了, 讀取資料, 轉換資料, 儲存資料到資料庫 :

    private static void FillDataBase_iboxDB(string tableName, string[] variables,
        List<Dictionary<string, string>> valueRows, string key)
    {
        var type = DataBaseRawTypeILGenerator(tableName, variables);    // 根據變數建立型別
        var insertCall = GetGenericFunction(typeof(iBoxDB.LocalServer.AutoBox), "Insert",
                new System.Type[] { type }, new object[] { tableName, System.Activator.CreateInstance(type) }, false);    // Insert<V> 方法
        if(insertCall != null)
        {
            var db = new iBoxDB.LocalServer.DB();
            var databaseAccess = db.Open();
            foreach(var values in valueRows)
            {
                var data = System.Activator.CreateInstance(type);    // 建立例項
                foreach(var valueKV in values)
                {
                    SetField(data, valueKV.Key, valueKV.Value);    // 反射修改變數
                }
                insertCall.Invoke(databaseAccess, new object[] { tableName, data });    // 寫入資料庫
            }
            db.Dispose();
        }
    }