zoukankan      html  css  js  c++  java
  • 超轻量级数据库 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: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] , 在运行时可以获取到, 至于上面的泛型怎样才能获取到我就不知道了, 因为泛型限定元组好像不存在.

      

  • 相关阅读:
    数字图像处理的Matlab实现(2)—MATLAB基础
    数字图像处理的Matlab实现(1)—绪论
    统计分析与R软件-chapter2-6
    统计分析与R软件-chapter2-5
    统计分析与R软件-chapter2-3
    题目1143:Primary Arithmetic
    剑指OFFER之翻转单词顺序
    剑指OFFER之把数组排成最小的数
    剑指OFFER之丑数
    最大的两个数
  • 原文地址:https://www.cnblogs.com/tiancaiwrk/p/13994955.html
Copyright © 2011-2022 走看看