超轻量级数据库 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秒, 果然反射式的永远比不了数据式的啊, 虽然用起来很方便的说.

posted @ 2020-11-17 16:29  tiancaiKG  阅读(1377)  评论(2编辑  收藏  举报