这是我的 ORM 框架系列的的第一篇文章,这一系列将会带着大家完整的实现一个完整 ORM 框架,你有任何技术上的疑问可以通过 邮件 联系我。


本文着重于描述一个 ORM 框架中会用到的几个技术点以及其使用的原由,而不是框架的整体设计,并且用于说明的这个小型框架是我大二时期所写,比较简陋,与其说是 ORM 框架,不如说是一个 SQL Helper,但它简单更容易理解。

我们来看一下这个框架效果,你可以在 此处 获取到它的源码。

首先是我们常见的数据模型类的定义:

 public class TestUser
 {
     public String Id { get; set; }
     public String Name { get; set; }
     public Int32 Age { get; set; }
     public DateTime Born { get; set; }
     public String Descrption { get; set; }
 }

现在我们通过它查询一些数据出来(代码):

   //这里我们使用的是 SQLite 数据库,你可以很容易从 DbProvider 类继承过来,以支持新的数据库
   var connStr = "Data Source = test.db; UTF8Encoding = True;";
   SQLiteDbProvider proivder = new SQLiteDbProvider("MyName", connStr);
   //查询数据
   List<TestUser> userInfos = proivder.
                              Select<TestUser>()
                              .Where(t=>t.Name=="John Wang" && t.Age>=20).ToList();

可以看到对数据的查询是通过方法来实现的,而筛选是通过一个 Lambda 表达式,过程中并没有编写任何 SQL 语句,也没有DbConnection、DbDataReader、DataTable 之类的对象,我们通过一种更符合高级编程语言的习惯的方法获取到了所需数据,但是只要我们继续在关系型数据库上存储数据,在 ADO.NET 上连接数据服务,这些无法避免。

上述查询数据的函数调用最终会被转化成如下 SQL:

SELECT  test_user.[id] , test_user.[name] , test_user.[age] , test_user.[born] , test_user.[descrption]  FROM test_user  WHERE  ( test_user.[name] =$name1 AND  test_user.[age] >=$age2) 

并由 ADO.NET 提交至对应的数据库服务。

接下来我们详细解析此转化是如何实现的。

一. Lambda 表达树的解析

SelectWhere 两个函数很好理解,可以直接对应成SQL(例如: SELECT * FROM xxx WHERE),重点看下 Where 函数传入的参数,的先来看一下 Where 方法的定义:

 IDriver<T> Where(Expression<Func<T, Boolean>> predicate);

参数要求传入一个表达式(强烈建议你先通过 这里 查看有关表达式的信息,此处不再赘述),其泛型参数为一个泛型委托类型,参数为 T (也就是 TestUser 类型) 并返回布尔值,这很好理解,我们通常筛选数据依据就是在条件成立与不成立之间。

而在例子中传入的表达式(记为式1):

t=>t.Name=="John Wang" && t.Age>=20

逐步分解表达式主体:

1.式一是计算符号为与(AndAlso)的二元表达式,左子节点为 t.Name==”John Wang” (式二),右子节点为 t.Age>=20 (式三)。

2.式二是计算符号为相等(Equal)的二元表达式,其左子节点为 t.Name (式四) 这是一个属性表达式链接文档中其名称为 MemeberExpression 但其实如果在调试器中,我们会发现其类型为 PropertyExpression 所以这里我就叫属性了),而右子节点为 “John Wang” (式五)这是一个常量表达式

3.式三是计算符号为大于等于(GreaterThanOrEqual)的二元表达式,其左子节点为 t.Age (式六) 是属性表达式,而右子节点为 20 (式七)又是一个常量表达式。

可以看到不同表达式相互关联形成一个树结构(建议通过调式器查看此表达式变量),我们只需要按照遍历树的方式去遍历这颗表达树获取信息就行了。

但是为什么要解析此表达式树呢,又需要那些信息,通过上面的 SQL 我们知道转化后此表达式对应的部分:

test_user.[name] =$name1 AND  test_user.[age] >=$age2

接下来我们根据需要将两者的关联建立起来:

  1. 通过表达式的泛型参数(模型类)我们可以确定与数据库的表的映射。
  2. 通过表达式的计算符号(例如: &&-AndAlso),可以确定与 SQL 中计算符号的映射(例如: AND)。
  3. 通过属性表达式(例如:t.Name),可以确定 SQL 条件对应的数据列映射(例如:test_user.[name])。
  4. 通过常量表达式(例如:”John Wang”),可以确定 SQL 条件项具体的值(test_user.[name] =$name1)。

之后是递归去解析表达式数据,获取到相应信息后通过映射关系转化成对应的 SQL 文本,拼接处整个条件 SQL即可,需要注意一下 SQL 中符号优先级的问题,例如: AND 、OR,你可以在此处获得到一个简单版本的实现。

当然除了在 Where 条件中使用表达式,其他地方也可以使用,例如,我们只需要查询出特定的列:

var result=proivder.Select<TestUser>(t=>new {
                           Name=t.Name,
                           Age=t.Age
                           }).ToList();

上面的代码中,便使用到了 NewExpression ,其解析过程不在赘述,大同小异。

将 Lambda 表达式转化为 SQL 文本,重点在于以下几点:

  1. 确定需要转化的目标 SQL 的格式。
  2. 确定能否从此表达式中获取足够多的信息,以建立映射。
  3. 映射建立是否合理,不是所有 SQL 都适合被生搬硬套到表达式上,晦涩的表达式,在使用时和解析时都会让人十分难受。

二. 模型类与表结构映射关系的建立

模型类与数据库表在基本情况下有很自然的映射关系。

类—–表,

类属性—–表字段

类的对象——表的记录

最常见的就是这三个映射,但是光凭类本身的元数据信息,还无法完整合理对映射提供信息,例如:数据库表的字段还有大小、是否主键、是否外键,是否唯一键的信息,当然你可以在类中定义更多的成员以补充这些映射,但是这就破坏的之前,类属性就是表字段的映射约定,这让事情变得晦涩难懂。

而在 .NET 中就有对元数据信息补充的东西——特性。

请记住特性 只是作为对映射信息补充的手段之一,我们也可以从文件中加载实现编写好的映射信息,从互联网上下载,这些映射信息只需要在第一次使用之前建立好就行。

接下来我们要明确,建立好映射能给我们带来什么。

第一点:很明显,它能为我们生成 SQL 提供信息,例如上面的 Lambda 表达式在解析时,知道了用到什么类、类的什么属性,然后它可以查找映射表,继续后面的 SQL 转换操作即可。

第二点:为 表记录与类对象 转换提供依据,我们在最初的示例中看到方法直接返回了一个由类对象构成链表,而不是一个原始的 DbReader 或 DataTable 对象,正是这些映射信息为这个转换提供了自动处理的可能,否则你需要在代码中手工编写,一一对应赋值,这节省了大量的体力,其中的实现过程不在赘述,通过一些反射操作即可,其中带来的性能损失也可以通过一些手段弥补,此处 是这个框架中处理的办法,当然你也可以通过 Emit 将损耗降到最低,或者通过事先编写的工具,直接生成静态代码(我觉得这是最无聊的办法)。

第三点:我的框架的目标不光只是为了一种特定的数据服务提供功能,我希望也它后续可以支持 NoSQL、内存、文件,映射的建立只是为了将数据从一种上下文中迁移到另一种上下文中去(你可以通过此处了解这个观点),将这个建立过程单独抽象出来有利于后续的实现。


好了,这一篇文章就写到这里吧,还是不太善于用文字表达心理的想法,很多东西心里明白,但是就是不知道怎么写出来,如果里面有什么疑惑的地方,你可以通过邮件 或者 TIM 与我沟通。