之前用的 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秒, 果然反射式的永遠比不了數據式的啊, 雖然用起來很方便的說.