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