之前用的 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:UsersXXXXDesktopTempabc.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:UsersXXXXDesktopTemp/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] , 在运行时可以获取到, 至于上面的泛型怎样才能获取到我就不知道了, 因为泛型限定元组好像不存在.