超輕量級數據庫 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();
        }
    }

    

  PS : 意外發現它的 Key 可以支持中文, C# 變量也支持中文, 這樣中文就不用轉換了

 

  PS : 突然想到從數據庫中獲取數據的時候, 其實類型是可以任意的, 比如

    public class Record1
    {
        public string name;
        public string x;
    }
    public class Record2
    {
        public string name;
        public string 中文測試;
    }

  那么泛型獲取其實就跟寫了一個過濾邏輯一樣只獲取對應的數據 :

    var bytes = System.IO.File.ReadAllBytes(@"C:\Users\XXXX\Desktop\Temp\abc.db");
    var db = new DB(bytes);
    var access = db.Open();
    access.Select<Record1>("from table");
    access.Select<Record2>("from table");

  如果使用元組來寫的話, 是不是會簡單一點? 不用另外定義了, 不過坑的就是它的 API對類型做了限定 : 

  元組不能通過 class 限定, 來測試一下 : 

public class Test : MonoBehaviour
{
    public static void TestCall<T>() where T : new()
    {
        Debug.Log(typeof(T).Name);
    }
    
    void Start()
    {
        var t1 = typeof((string name, string x, string z, string 中文測試));
        CallGenericFunction(typeof(Test), "TestCall", null, new Type[] { t1 }, null, true);
    }
}

  這是可行的 : 

  然而當限定了 class 之后是不行的 : 

    public static void TestCall<T>() where T : class, new() // 限定class
    {
        Debug.Log(typeof(T).Name);
    }

  好吧, 元組就是個結構體......

  不過這都不是問題, 通過我反射大師的計算, 還是可以通過上面的運行時創建類型來實現的, 首先看看最終效果 : 

    [UnityEditor.MenuItem("Test/Write")]
    public static void WriteTest()
    {
        var bytes = System.IO.File.ReadAllBytes(@"C:\Users\XXXX\Desktop\Temp/abc.db");
        var db = new DB(bytes);
        var access = db.Open();

        var ins = iBoxDBHelper.GetFromTuple<(string name, string x, string z)>("ZW_Position", "起點", new string[] { "name", "x", "z" }, access);
        Debug.Log(ins.name);
        Debug.Log(ins.x);
        Debug.Log(ins.z);
    }

  

  結果是能夠使用元組來替代指定類型的, 使用起來會非常方便. 代碼也是沿用了創建運行時類型的方法, 不過這使用到了 Emit, 在必須進行 IL2CPP 的平台是無法編譯的......比如各種主機平台.

  中間的轉換獲取代碼 : 

    public static T GetFromTuple<T>(string tableName, string searchKey, string[] keys, AutoBox autoBox)
    {
        const string TypeName = "Temp";
        object ins = System.Activator.CreateInstance<T>();      // 必須裝箱, 否則無法設置 Field
        var fields = typeof(T).GetFields();
        var type = iBoxDBHelper.DataBaseRawTypeILGeneratorRunTime(TypeName, keys);  // 創建臨時類型
        var tag = iBoxDBHelper.CallGenericFunction(autoBox.GetType(), "Get", autoBox, new Type[] { type }, new object[] { tableName, new object[] { searchKey } }, false);
        if(tag != null)
        {
            for(int i = 0, imax = Math.Min(keys.Length, fields.Length); i < imax; i++)
            {
                var varName = keys[i];
                fields[i].SetValue(ins, iBoxDBHelper.GetField(tag, varName));   // 從臨時類型轉換為元組
            }
        }
        return (T)ins;
    }

  在這里發現匿名元組還是跟老版本一樣很不好用, 就算在外部定義好了變量 : <(string name, string x, string z)> 這些變量 name, x, z 也是無法通過反射獲取到的, 它的 field 仍然是 Item1, Item2, Item3... 所以才會需要手動傳入 keys 來告訴反射給新的類創建哪些變量......非常多此一舉. 並且因為沒有名稱的一一對應, 所以元組的變量順序必須跟 keys 傳入的順序一致才行......

var ins = iBoxDBHelper.GetFromTuple<(string name, string x, string z)>("ZW_Position", "起點", new string[] { "name", "x", "z" }, access);

  如果可以省略 

new string[] { "name", "x", "z" }

  這一段就完美了.

 

  補充個元組小知識, 如果是硬編譯的元組, 是可以在運行時獲取元組的變量的, 比如下面這樣 : 

    public class C
    {
        public (int a, int b) M()
        {
            return (1, 2);
        }
    }
    // ......
    [UnityEditor.MenuItem("Test/Test")]
    public static void JustTest()
    {
        Type t = typeof(C);
        MethodInfo method = t.GetMethod(nameof(C.M));
        var attr = method.ReturnParameter.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>();

        var names = attr.TransformNames;
        foreach (var name in names)
        {
            Debug.Log(name);        // 可以獲取到 a, b 
        }
    }

  它是在編譯時自動給函數添加了一個屬性 [TupleElementNames] , 在運行時可以獲取到, 至於上面的泛型怎樣才能獲取到我就不知道了, 因為泛型限定元組好像不存在.

(2021.03.05)

  回頭看了一下, 硬編譯的元組這里, 獲取方法的方式也可以通過表達式樹大法來獲取, 看起來更優雅一些 : 

using UnityEngine;
using System;
using System.Reflection;
using System.Linq.Expressions;

public class TestSample : MonoBehaviour
{
    public static class C
    {
        public static (int a, int b) ReturnSample()
        {
            return default;
        }

        public static void InputSample((int c, int d) data)
        {

        }
    }

    void Start()
    {
        // test return
        {
            var info = GetMethodInfo(() => C.ReturnSample());
            var att = info.ReturnParameter.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>();
            if(att != null)
            {
                foreach(var varName in att.TransformNames)
                {
                    Debug.Log(varName);
                }
            }
        }

        // test parameters
        {
            var info2 = GetMethodInfo(() => C.InputSample(default));
            var parameters = info2.GetParameters();
            if(parameters != null)
            {
                foreach(var parameter in parameters)
                {
                    var att2 = parameter.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>();
                    if(att2 != null)
                    {
                        foreach(var varName in att2.TransformNames)
                        {
                            Debug.Log(varName);
                        }
                    }
                }
            }
        }
    }

    public static MethodInfo GetMethodInfo(Expression<Action> expression)
    {
        return GetMethodInfo((LambdaExpression)expression);
    }
    public static MethodInfo GetMethodInfo(LambdaExpression expression)
    {
        MethodCallExpression outermostExpression = expression.Body as MethodCallExpression;
        if(outermostExpression == null)
        {
            throw null;
        }
        return outermostExpression.Method;
    }
}

 

-----------------

(2021.03.25)

  之前一直沒有把數據庫替換成 iboxDB, 是因為使用 sqlite3 的時候獲取的數據是 string 類型的, 然后直接轉換成了我的 DataTable 類型 : 

public partial class Data
{
    public Common.DataTable FID { get; private set; }
    public Common.DataTable FEATID { get; private set; }

    public void Init(Mono.Data.Sqlite.SqliteDataReader reader)
    {
        this.FID = reader["FID"].ToString();
        this.FEATID = reader["FEATID"].ToString();    
    }
}

  這樣數據的類型轉換就省去了, 非常方便. 可是 iboxDB 的反序列化對象是強類型的, 不能直接轉換為 DataTable, 不像 LitJson 或者 Xml 這樣提供了自定義轉換, 所以需要進行二次轉換 :

// iboxDB 輸出對象
public partial class Data_iboxDB
{
    public string FID { get; set; }
    public string FEATID { get; set; }
    
    public Data Convert()
    {
        var retVal = new FireHydrantData();
        retVal.FID = this.FID;
        retVal.FEATID = this.FEATID;
        return retVal;
    }
}

// 我們想要的對象
public partial class Data
{
    public DataTable FID { get; set; }
    public DataTable FEATID { get; set; }
}

  過程就成了 [iboxDB] -> Data_iboxDB -> Data, 然后就沒有去繼續測試了, 然而今天進行了測試, 發現效率上天差地別, 至少在全表數據獲取的時候 iboxDB 比 sqlite 快了很多, 如下 : 

  Sqlite3 用了46秒, iboxDB 用了2秒, 都是獲取一個10012個數據的全表數據......

  找到一個說法 : 

在SQLite為.Net提供的驅動中使用列名進行讀取的時候SqliteDataReader內部對結果集中每一列進行遍歷並且不是遍歷數組而是P/Invoke調用SQLite非托管函數.導致數據庫數據讀取性能下降.下降的程度根據結果集列數而變化.

  好家伙. 剛好我就是用列名去獲取數據了 : 

    this.FID = reader["FID"].ToString();
    this.FEATID = reader["FEATID"].ToString();

   我改改代碼, 看看使用 index 的方式會怎樣 : 

public partial class Data
{
    public Common.DataTable FID { get; private set; }
    public Common.DataTable FEATID { get; private set; }

    public void Init(Mono.Data.Sqlite.SqliteDataReader reader)
    {
        this.FID = reader[0].ToString();  // index
        this.FEATID = reader[1].ToString();   // index 
    }
}

  好家伙, 0.8秒 VS 46秒, 總算回歸正確的地位了......

  然后把 iboxDB 的中間轉換去掉, 直接獲取 Data_iboxDB 的情況下, 仍然需要1.9秒, 果然反射式的永遠比不了數據式的啊, 雖然用起來很方便的說.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM