最原始的 ORM 需求就是将关系数据库查询出来的结果数据转换成强类型对象使用,解决查询结果数据在面向对象语言中使用不够方便的问题。
接下来的需求就是使用面向对象的方式查询数据库(也就是抛弃 SQL 语句这种不方便维护的字符串),这个阶段的 ORM 框架可谓五花八门, 为了能够在面向对象语言中模拟 SQL 语句费尽心思。
我曾经也在这些方面折腾过一段时间,可是慢慢的发现这种做法不仅带来了各种限制,而且相比于直接写 SQL 语句并不能提高多少开发效率,该写的都少不了,唯一的优势就是有 IDE 的智能提示不那么容易出错。
面向对象的本质是抽象,而不是折腾各种写法;而关系模型并不具备面向对象特性,强行将两者融合为一体并不能得到超体,而是各种别扭。我觉得单纯从写法的角度考虑,像 LINQ 这种直接被 IDE 与编译器内置支持的才是正道。
有时候放弃也是一种不错的选择,比如在这个问题里面我就放弃了关系模型,选择了面向对象。这个决定让我从 ORM 问题中解脱出来(当时感觉就像是挣脱了枷锁,前方满是自由与希望),我确定这是一条适合我的道路。
放弃了关系模型,也就放弃了关系数据库特有的功能特性,数据库也就退化成了支持索引查询的储存设备,数据处理的重心由数据库转到了程序逻辑中。

接下来先说说缓存,一般来说缓存是为了提高数据查询效率而保存的查询结果,避免重复的计算操作。
很多人的脑海里,一提到缓存就是 Key-Value。可能是由于主流的缓存框架用的都是这种模型,因为对于分布式缓存结构而言,K-V 模式是简单高效率而且容易实现的。
但是我要说的缓存,既不是保存查询结果,也不是分布式 K-V 模型。
我要说的缓存是针对查询需求,在本地内存中使用合适的数据结构组织数据源,尽可能的降低查询需求以及相关操作的时间复杂度,当然要做到这一点需要一定的数据结构与算法知识。
缓存数据源对于内存的使用量与性能效果基本都是可控的,而查询结果的体量则是不可控的,实时性也是难以保障的,当然对于某些计算量大而且体量不可控的查询结果还是需要 K-V 分布式缓存处理的。
缓存数据源相当于内存索引,查询都在内存中处理,可以减少数据库物理层的索引需求,数据库物理层基本只需要主键索引。
缓存数据源可以方便的实时更新计算数据,对于计算列不存在事务问题,减少数据库表格中的计算列这种冗余需求,同时减少数据更新操作。
缓存数据源可以很方便的在程序逻辑中访问数据,带来类似于 LINQ to Object 的编码体验,可以快速应对数据查询需求的变化,不再为复杂的查询需求而烦恼。
当然缓存数据源可能需要使用大量内存(我认为大内存比人工优化代价更低),但并不需要缓存所有数据,但至少需要缓存与查询条件有关的所有数据。

抛弃了关系模型的 ORM 只存在单表格操作,可以真正的抛弃 SQL 及其相关理念,你只需要操作数据对象。因为应用层没有 SQL 语句,所以不存在不可控的数据更新,从而做到简单可控的数据同步,可以与数据源缓存自然的融合到一起。

由于 ORM 内存索引缓存框架 的实现依赖 LambdaExpression,所以暂时不支持 .NET Framework 2.0

该功能需要在工程项目中配置静态代码生成

1. 数据模型

为了方便数据在网络中传输,AutoCSer 需要合理规划字段与属性的作用,在默认规则中,需要序列化的原始数据使用字段表示,而属性则用于在应用层表示原始数据经过某种计算以后的结果,所以定义数据成员应该使用字段而非属性。

数据模型的字段将映射为表格数据列(也许你把它叫做实体类,但是有的人认为与业务逻辑相关的才能叫做实体类),需要给这个类型添加 SQL 表格模型申明配置 [AutoCSer.Sql.Model]

    [AutoCSer.Sql.Model]
    public partial class MyModel
    {
        /// <summary>
        /// 关键字1
        /// </summary>
        [AutoCSer.emit.dataMember(PrimaryKeyIndex = 1)]
        public int Key1;
        /// <summary>
        /// 关键字2
        /// </summary>
        [AutoCSer.emit.dataMember(PrimaryKeyIndex = 2)]
        public int Key2;
    }

项目编译完以后,将在这个 MyModel 类型中生成一个继承自 MyModel 的嵌套泛型数据模型类,其中包括一个定义 SQL 操作工具的静态字段 sqlTable,如果在 MyModel 定义了关键字还将生成一个整合关键字的 struct PrimaryKey,比如

        public partial class MyModel
        {
            /// <summary>
            /// 关键字
            /// </summary>
            [AutoCSer.code.ignore]
            public struct PrimaryKey : IEquatable<PrimaryKey>
            {
                /// <summary>
                /// 关键字1
                /// </summary>
                public int Key1;
                /// <summary>
                /// 关键字2
                /// </summary>
                public int Key2;
                /// <summary>
                /// 关键字比较
                /// </summary>
                /// <param name="other" />关键字</param>
                /// <returns>是否相等</returns>
                public bool Equals(PrimaryKey other)
                {
                    return Key1/**/.Equals(other.Key1) && Key2/**/.Equals(other.Key2);
                }
                /// <summary>
                /// 哈希编码
                /// </summary>
                /// <returns></returns>
                public override int GetHashCode()
                {
                    return Key1.GetHashCode() ^ Key2/**/.GetHashCode();
                }
                /// <summary>
                /// 关键字比较
                /// </summary>
                /// <param name="obj" /></param>
                /// <returns></returns>
                public override bool Equals(object obj)
                {
                    return Equals((PrimaryKey)obj);
                }
            }
            /// <summary>
            /// 数据库表格模型
            /// </summary>
            /// <typeparam name="tableType">表格映射类型</typeparam>
            public abstract class SqlModel<tableType> : MyModel
                where tableType : SqlModel<tableType>
            {
                /// <summary>
                /// SQL表格操作工具
                /// </summary>
                protected static readonly AutoCSer.Sql.Table<tableType, MyModel, PrimaryKey> sqlTable = AutoCSer.Sql.Table<tableType, MyModel, PrimaryKey>.Get();
            }
        }

建议使用一个单独的项目存放数据模型类,就叫它数据模型层。

2. 数据列

数据模型中的字段,将映射为表格数据列,可以使用数据成员申明配置 [AutoCSer.Sql.Member] 对该字段添加描述。

比如申明为关键字,并且指定关键字的索引顺序,不指定表示随机顺序。

        [AutoCSer.Sql.Member(PrimaryKeyIndex = 1)]
        public int Key;

比如指定字符串长度与编码,IsAscii = true 则采用单字节字符,否则使用双字节字符。

        [AutoCSer.Sql.Member(IsAscii = true, MaxStringLength = 256)]
        public string Email;

比如指定缓存分组,用于配合缓存组件使用,因为有的字段可能需要整表缓存(比如查询相关的字段),有的字段可能因为内存容量问题需要使用具有淘汰策略的缓存机制。

        [AutoCSer.Sql.Member(Group = 1)]
        public string Content;

比如有的字段作为实时计算字段存在,不需要映射为表格数据列而且需要为 TCP 客户端同步缓存处理,那么需要设置 [AutoCSer.Sql.Log] 生成处理函数。

            [AutoCSer.Sql.Log(CountType = typeof(XXX))]
            public int XXXCount;

为了让数据列能够自然的表达需求,数据列的类型定义不限于数据库支持的数据类型,比如可以直接使用枚举类型

        public enum type : byte
        {
            X,
            Y
        }
        public type Type = type.X;

比如可以自定义数据类型,需要使用 [AutoCSer.Sql.Member] 申明数据库数据类型,并且需要实现类型之间的强制类型转换,比如

    [AutoCSer.Sql.Member(DataType = typeof(string))]
    public struct MyString
    {
        public string Value;
        public static implicit operator MyString(string value) { return new MyString { Value = value }; }
        public static implicit operator string(MyString value) { return value.Value; }
    }
        public MyString MyString;

比如可以使用 SQL 组合数据申明配置 [AutoCSer.Sql.Column],为了防止循环嵌套问题只支持 struct,比如

    [AutoCSer.Sql.Column]
    public struct Range
    {
        public int Start;
        public int End;
    }
        public Range MyRange;

对于 SQL 组合数据最终将映射成多个表格数据列,比如这个 MyRange 字段将映射为 MyRange_Start 与 MyRange_End 两个数据列,每个层级名称之间使用下划线(_)连接,对于 SQL 组合数据的更新只能进行整体操作。

注意:其它未知类型将自动进行 JSON 序列化处理。

3. 数据表格

它继承自泛型数据模型类,与数据库表格一一对应,需要给这个类型添加 SQL 表格申明配置 [AutoCSer.Sql.Table]

    [AutoCSer.Sql.Table(ConnectionName = "MyConnection")]
    public class MyTable : MyModel.SqlModel<MyTable>

当多个表格存在相同结构的需求时,应该使用同一个数据模型,注意泛型数据模型类的参数 tableType 就是当前类型,否则会造成运行时类型错误。

在程序运行加载相关程序集时,会自动根据数据表格信息创建数据库表格,对于已经存在的表格缺少数据列的情况也会自动补全;由于数据安全问题,程序不会自动进行数据列修改与删除操作,请在程序运行前自行维护数据库表格。

4. 数据库表格操作工具

数据模型中生成的泛型数据模型类中有一个静态字段 sqlTable 就是用来操作这个表格的工具实例,它继承自 AutoCSer.Sql.Table<tableType, modelType>,提供数据表格的操作 API。

5. 数据缓存

重点重复一遍:数据缓存需要根据查询需求设计内存数据结构,目标是尽可能在 O(log(n)) 甚至 O(1) 的时间复杂度内定位查询需求数据。

因为这一块属于个性化需求,没有通用的类库可以解决所有问题,我平时常用的缓存数据结构在 AutoCSer.Sql.Cache 命名空间中,Whole 命名空间是与整表缓存相关的,Counter 命名空间是具有淘汰策略的。

从性能的角度考虑,在 Id 与 GUID 的选择上,我认为应该抛弃 GUID,因为 Id 可以当成索引访问基于数组的缓存,不需要任何锁操作,而 GUID 基本只能用字典不可能避开锁的问题。

由于该功能存在环境依赖问题(比如需要 SqlServer / MySql),可能需要一定的数据结构与算法知识,还涉及到项目组合的问题,不方便提供展示全面功能 DEMO,简单的参考 AutoCSer.TestCase.SqlModel / AutoCSer.TestCase.SqlTableCacheServer / AutoCSer.TestCase.SqlTableWeb 。

相关测试项目包括 AutoCSer.TestCase.SqlModel / AutoCSer.TestCase.SqlTableCacheServer / AutoCSer.TestCase.SqlTableWeb