Rafy 领域实体框架设计 - 重构 ORM 中的 Sql 生成
前言
Rafy 领域实体框架作为一个使用领域驱动设计作为指导思想的开发框架,必然要处理领域实体到数据库表之间的映射,即包含了 ORM 的功能。由于在 09 年最初设计时,ORM 部分的设计并不是最重要的部分,那里 Rafy 的核心是产品线工程、模型驱动开发、界面生成等。所以当时,我们简单地采用了一个开源的小型 ORM 框架:《Lite ORM Library》。这个 ORM 框架可以生成比较简单的 Sql 语句,以处理一般性的情况。
随着不断使用,我们也不断对 ORM 的源码做了不少改动,让它在支持简单语句生成的同时,也支持让开发人员直接使用手动编写的 Sql 语句来查询领域实体。但是过程中,一直没有修改最核心的 Sql 语句生成模块。随着应用的不断深入,遇到的场景越来越多,需要生成复杂 Sql 语句的场景也越来越多。而这些场景如果还让开发人员自己去编写复杂 Sql 语句,不但框架的易用性下降,而且由于写了过多的 Sql 语句,还会让开发人员面向领域实体来开发的思想减弱。
这两周,我们对 Sql 语句生成模块实施了重构。与其说是重构,不如说重写,因为 90% Lite ORM 的类库都已经不再使用。但是又不得不面对对历史代码中接口的兼容性问题。接下来,将说明本次重构中的关键技术点。
旧代码讲解
最初采用的 Lite ORM 是一个轻量级的 ORM 框架,采用在实体对象上标记特性(Attribute)来声明实体的元数据,并使用链式接口来作为查询接口以方便开发人员使用。这是一个简单、易移植的 ORM 框架,对初次使用、设计 ORM 的同学来说,可以起到一个很好的借鉴作用。相关的设计,可以参考 Lite ORM 的原文章:《Lite ORM Library V2 》。
由于这几年我们已经对该框架做了大量的修改,所以很多接口已经与原框架不一致了。IQuery 作为描述查询的核心类型,被重命名为 IPropertyQuery,所有方法的参数也都直接面向 Rafy 实体的《托管属性》。但是在整体结构上,还是与原框架保持一致。例如,它还只是一个一维的结构:
1: /// <summary>
2: /// 使用托管属性进行查询的条件封装。
3: /// </summary>
4: public interface IPropertyQuery : IDirectlyConstrain
5: {
6: /// <summary>
7: /// 是否还没有任何语句
8: /// </summary>
9: bool IsEmpty { get; }
10:
11: /// <summary>
12: /// 当前的查询是一个分页查询,并使用这个对象来描述分页的信息。
13: /// </summary>
14: PagingInfo PagingInfo { get; }
15:
16: /// <summary>
17: /// 用于查询的 Where 条件。
18: /// </summary>
19: IConstraintGroup Where { get; set; }
20:
21: /// <summary>
22: /// 对引用属性指定的表使用关联查询
23: ///
24: /// 调用此语句会生成相应的 INNER JOIN 语句,并把所有关联的数据在 SELECT 中加上。
25: ///
26: /// 注意!!!
27: /// 目前不支持同时 Join 两个不同的引用属性,它们都引用同一个实体/表。
28: /// </summary>
29: /// <param name="property"></param>
30: /// <param name="type">是否同时查询出相关的实体数据。</param>
31: /// <param name="propertyOwner">
32: /// 显式指定该引用属性对应的拥有类型。
33: /// 一般使用在以下情况中:当引用属性定义在基类中,而当前正在对子类进行查询时。
34: /// </param>
35: /// <returns></returns>
36: IPropertyQuery JoinRef(IRefProperty property, JoinRefType type = JoinRefType.JoinOnly, Type propertyOwner = null);
37:
38: /// <summary>
39: /// 按照某个属性排序。
40: ///
41: /// 可以调用此方法多次来指定排序的优先级。
42: /// </summary>
43: /// <param name="property">按照此属性排序</param>
44: /// <param name="direction">排序方向。</param>
45: /// <returns></returns>
46: IPropertyQuery OrderBy(IManagedProperty property, OrderDirection direction);
47:
48: //其它部分省略...
49: }
可以看到,该类型以一维的形式来描述了一个 Sql 查询的相关元素:Join 数据源、Where 条件、OrderBy 规则、分页信息。
只有其中的 Where 条件被设计为树型结构来处理相对复杂的 And、Or 连接的条件。
可以看到,虽然有 SqlWhereConstraint 来添加任意的 Sql 语句作为 Where 约束条件,但是这样的结构还是比较简单,不足以描述所有的 Sql。
重构方案
我们的目标是实现复杂 Sql 的生成,理论上需要支持所有能想到的 Sql 语句的生成。
初期方案其实很简单,就是使用解释器模式与访问器模式配合来重构底层代码。根据 Sql 的语法规定,构造 Sql 语法树节点中的相关类型,这样就可以用一棵树来解释任意的 Sql 语句;同时使用访问器模式来遍历某个具体 Sql 语法树。过程中还需要特别注意,尽量不要构造不必要的树节点,以增加垃圾回收器的压力。
在此初步方案上,还需要考虑:分层架构、组件间依赖、以及旧代码的兼容性设计。
以下是整个方案的分层设计:
SqlTree:核心的、可重用的 Sql 语法树层。定义了通用的 Sql 语法结构,并解决从语法树到 Sql 语句的转换、生成,以及屏蔽不同数据库间不同子句的生成规则。
EntityQuery:把 SqlTree 作为类库引用,同时整合领域实体、实体属性的设计。
Query Interface:以 IQuery 接口的方式提供给应用层。
Linq Query:为了给开发人员提供更易用的接口,需要提供 Linq 语法的支持。本层用于解析 Linq 表达式树,并生成最终的实体查询的对象。
Property Query:为了兼容旧的接口,该部分在提供旧接口的前提下,换为使用新的 IQuery 来实现。
Application:开发人员的应用层代码。可以使用最易用的 Linq、旧的 PropertyQuery,同时也可以直接使用 IQuery 接口来完成复杂查询。
组件详细设计
Sql 语法树
使用解释器模式设计,用于描述 Sql 查询语句。
所有树节点都从 SqlNode 继承,并拥有自己的属性来描述不同的节点位置。例如 SqlSelect 类型,代码如下:
1: /// <summary>
2: /// 表示一个 Sql 查询语句。
3: /// </summary>
4: class SqlSelect : SqlNode
5: {
6: private IList _orderBy;
7:
8: public override SqlNodeType NodeType
9: {
10: get { return SqlNodeType.SqlSelect; }
11: }
12:
13: /// <summary>
14: /// 是否只查询数据的条数。
15: ///
16: /// 如果这个属性为真,那么不再需要使用 Selection。
17: /// </summary>
18: public bool IsCounting { get; set; }
19:
20: /// <summary>
21: /// 是否需要查询不同的结果。
22: /// </summary>
23: public bool IsDistinct { get; set; }
24:
25: /// <summary>
26: /// 如果指定此属性,表示需要查询的条数。
27: /// </summary>
28: public int? Top { get; set; }
29:
30: /// <summary>
31: /// 要查询的内容。
32: /// 如果本属性为空,表示要查询所有列。
33: /// </summary>
34: public SqlNode Selection { get; set; }
35:
36: /// <summary>
37: /// 要查询的数据源。
38: /// </summary>
39: public SqlSource From { get; set; }
40:
41: /// <summary>
42: /// 查询的过滤条件。
43: /// </summary>
44: public SqlConstraint Where { get; set; }
45:
46: /// <summary>
47: /// 查询的排序规则。
48: /// 可以指定多个排序条件,其中每一项都必须是一个 SqlOrderBy 对象。
49: /// </summary>
50: public IList OrderBy
51: {
52: get
53: {
54: if (_orderBy == null)
55: {
56: _orderBy = new ArrayList();
57: }
58: return _orderBy;
59: }
60: internal set { _orderBy = value; }
61: }
62:
63: //...
64: }
Sql 生成器
使用访问器模式设计,用于遍历整个 Sql 语法树。以下是 SqlNodeVisitor 的代码:
1: /// <summary>
2: /// SqlNode 语法树的访问器
3: /// </summary>
4: abstract class SqlNodeVisitor
5: {
6: protected SqlNode Visit(SqlNode node)
7: {
8: switch (node.NodeType)
9: {
10: case SqlNodeType.SqlLiteral:
11: return this.VisitSqlLiteral(node as SqlLiteral);
12: case SqlNodeType.SqlSelect:
13: return this.VisitSqlSelect(node as SqlSelect);
14: case SqlNodeType.SqlColumn:
15: return this.VisitSqlColumn(node as SqlColumn);
16: case SqlNodeType.SqlTable:
17: return this.VisitSqlTable(node as SqlTable);
18: case SqlNodeType.SqlColumnConstraint:
19: return this.VisitSqlColumnConstraint(node as SqlColumnConstraint);
20: case SqlNodeType.SqlBinaryConstraint:
21: return this.VisitSqlBinaryConstraint(node as SqlBinaryConstraint);
22: case SqlNodeType.SqlJoin:
23: return this.VisitSqlJoin(node as SqlJoin);
24: case SqlNodeType.SqlArray:
25: return this.VisitSqlArray(node as SqlArray);
26: case SqlNodeType.SqlSelectAll:
27: return this.VisitSqlSelectAll(node as SqlSelectAll);
28: case SqlNodeType.SqlColumnsComparisonConstraint:
29: return this.VisitSqlColumnsComparisonConstraint(node as SqlColumnsComparisonConstraint);
30: case SqlNodeType.SqlExistsConstraint:
31: return this.VisitSqlExistsConstraint(node as SqlExistsConstraint);
32: case SqlNodeType.SqlNotConstraint:
33: return this.VisitSqlNotConstraint(node as SqlNotConstraint);
34: case SqlNodeType.SqlSubSelect:
35: return this.VisitSqlSubSelect(node as SqlSubSelect);
36: default:
37: break;
38: }
39: throw new NotImplementedException();
40: }
41:
42: protected virtual SqlJoin VisitSqlJoin(SqlJoin sqlJoin)
43: {
44: this.Visit(sqlJoin.Left);
45: this.Visit(sqlJoin.Right);
46: this.Visit(sqlJoin.Condition);
47: return sqlJoin;
48: }
49:
50: protected virtual SqlBinaryConstraint VisitSqlBinaryConstraint(SqlBinaryConstraint node)
51: {
52: this.Visit(node.Left);
53: this.Visit(node.Right);
54: return node;
55: }
56:
57: //...
58: }
基于实体的查询
1. IQuery 相关接口用于描述整个基于实体的查询。
例如,IColumnNode 表示一个列节点,其实是由一个实体属性来指定的:
1: namespace Rafy.Domain.ORM.Query
2: {
3: /// <summary>
4: /// 一个列节点
5: /// </summary>
6: public interface IColumnNode : IQueryNode
7: {
8: /// <summary>
9: /// 本列属于指定的数据源
10: /// </summary>
11: INamedSource Owner { get; set; }
12:
13: /// <summary>
14: /// 本属性对应一个实体的托管属性
15: /// </summary>
16: IManagedProperty Property { get; set; }
17:
18: /// <summary>
19: /// 本属性在查询结果中使用的别名。
20: /// </summary>
21: string Alias { get; set; }
22: }
23: }
2. EntityQuery 层中的类型实现了 IQuery 中对应的接口,并使用领域实体的相关 API 来实现从实体到表、实体属性到列的转换。同时,为了减少对象的数量,这些类型与 Sql 语法树的关系都使用继承,而不是关联。也就是说,它们直接从 SqlTree 对应的类型上继承下来,这样,在构造 EntityQuery 的同时,也构造好了底层的 Sql 语法树。
3. QueryFactory 封装了大量易用的 API 来构造 IQuery 接口。
使用示例
下面,就以几个典型的单元测试的相关代码来说明新的查询框架的使用方法:
使用 Linq 的数据层查询
1: public int LinqCountByBookName(string name)
2: {
3: return this.FetchCount(r => r.DA_LinqCountByBookName(name));
4: }
5: private EntityList DA_LinqCountByBookName(string name)
6: {
7: var q = this.CreateLinqQuery();
8: q = q.Where(c => c.Book.Name == name);
9: return this.QueryList(q);
10: }
使用 IQuery 的数据层查询
1: public int CountByBookName2(string name)
2: {
3: return this.FetchCount(r => r.DA_CountByBookName2(name));
4: }
5: private EntityList DA_CountByBookName2(string name)
6: {
7: var source = f.Table(this);
8: var bookSource = f.Table<BookRepository>();
9: var q = f.Query(
10: from: f.Join(source, bookSource)
11: );
12: q.AddConstraintIf(Book.NameProperty, PropertyOperator.Equal, name);
13: return this.QueryList(q);
14: }
可以看到,使用 IQuery 接口来查询,虽然灵活性最大、性能更好,但是相对于 Linq 来说会更加复杂。
使用 IQuery 来生成 Sql
1: [TestMethod]
2: public void ORM_TableQuery_InSubSelect()
3: {
4: var f = QueryFactory.Instance;
5: var articleSource = f.Table(RF.Concrete<ArticleRepository>());
6: var userSource = f.Table(RF.Concrete<BlogUserRepository>());
7: var query = f.Query(
8: from: userSource,
9: where: f.Constraint(
10: column: userSource.Column(BlogUser.IdProperty),
11: op: PropertyOperator.In,
12: value: f.Query(
13: selection: articleSource.Column(Article.UserIdProperty),
14: from: articleSource,
15: where: f.Constraint(articleSource.Column(Article.CreateDateProperty), DateTime.Today)
16: )
17: )
18: );
19:
20: var generator = new SqlServerSqlGenerator { AutoQuota = false };
21: f.Generate(generator, query);
22: var sql = generator.Sql;
23:
24: Assert.IsTrue(sql.ToString() ==
25: @"SELECT *
26: FROM BlogUser
27: WHERE BlogUser.Id IN (
28: SELECT Article.UserId
29: FROM Article
30: WHERE Article.CreateDate = {0}
31: )");
32: Assert.IsTrue(sql.Parameters.Count == 1);
33: Assert.IsTrue(sql.Parameters[0].Equals(DateTime.Today));
34: }
使用 SqlTree 来生成 Sql
1: [TestMethod]
2: public void ORM_SqlTree_Select_InSubSelect()
3: {
4: var select = new SqlSelect();
5: var articleTable = new SqlTable { TableName = "Article" };
6: var subSelect = new SqlSelect
7: {
8: Selection = new SqlColumn { Table = articleTable, ColumnName = "UserId" },
9: From = articleTable,
10: Where = new SqlColumnConstraint
11: {
12: Column = new SqlColumn { Table = articleTable, ColumnName = "CreateDate" },
13: Operator = SqlColumnConstraintOperator.Equal,
14: Value = DateTime.Today
15: }
16: };
17:
18: var userTable = new SqlTable { TableName = "User" };
19: select.Selection = new SqlSelectAll();
20: select.From = userTable;
21: select.Where = new SqlColumnConstraint
22: {
23: Column = new SqlColumn { Table = userTable, ColumnName = "Id" },
24: Operator = SqlColumnConstraintOperator.In,
25: Value = subSelect
26: };
27:
28: var generator = new SqlServerSqlGenerator { AutoQuota = false };
29: generator.Generate(select);
30: var sql = generator.Sql;
31: Assert.IsTrue(sql.ToString() == @"SELECT *
32: FROM User
33: WHERE User.Id IN (
34: SELECT Article.UserId
35: FROM Article
36: WHERE Article.CreateDate = {0}
37: )");
38: Assert.IsTrue(sql.Parameters.Count == 1);
39: Assert.IsTrue(sql.Parameters[0].Equals(DateTime.Today));
40: }
框架下载
框架使用测试驱动的方法开发,在开发时是先编写相关的测试用例,再实现内部代码。重构的同时,我们为能想到的场景都编写了测试用例:
目前,框架版本也升级到了 2.23.2155。
有兴趣的同学,了解、下载最新的框架,请参考:《Rafy 领域实体框架发布!》。(框架目前不开源,但可免费使用。)
Rafy 领域实体框架设计 - 重构 ORM 中的 Sql 生成的更多相关文章
- 重构 ORM 中的 Sql 生成
Rafy 领域实体框架设计 - 重构 ORM 中的 Sql 生成 前言 Rafy 领域实体框架作为一个使用领域驱动设计作为指导思想的开发框架,必然要处理领域实体到数据库表之间的映射,即包含了 OR ...
- Rafy 领域实体框架 - 树型实体功能(自关联表)
在 Rafy 领域实体框架中,对自关联的实体结构做了特殊的处理,下面对这一功能进行讲解. 场景 在开发数据库应用程序时,往往会遇到自关联表的场景.例如,分类信息.组织架构中的部门.文件夹信息等,都 ...
- Rafy 领域实体框架简介
按照最新的功能,更新了最新版的<Rafy 领域实体框架的介绍>,内容如下: 本文包含以下章节: 简介 特点 优势 简介 Rafy 领域实体框架是一个轻量级 ORM 框架. 与一般的 ORM ...
- Rafy 领域实体框架 - 公司内部培训视频
本月给公司内部一个项目做架构重构,其中使用到了 Rafy 框架.所以我培训了 Rafy 领域实体框架的使用方法,过程中录制了视频,方便其他同事查看.现在把视频放到园里来分享下,有兴趣的朋友可以看看,有 ...
- Rafy 领域实体框架示例(1) - 转换传统三层应用程序
Rafy 领域实体框架发布后,虽然有帮助文档,许多朋友还是反映学习起来比较复杂,希望能开发一个示例程序,展示如何使用 Rafy 领域实体框架所以,本文通过使用 Rafy 领域实体框架来改造一个传统的三 ...
- Rafy 领域实体框架演示(2) - 新功能展示
本文的演示需要先完成上一篇文章中的演示:<Rafy 领域实体框架示例(1) - 转换传统三层应用程序>.在完成改造传统的三层系统之后,本文将讲解使用 Rafy 实体框架后带来的一些常用功能 ...
- Rafy 领域实体框架演示(4) - 使用本地文件型数据库 SQLCE 绿色部署
本系列演示如何使用 Rafy 领域实体框架快速转换一个传统的三层应用程序,并展示转换完成后,Rafy 带来的新功能. <福利到!Rafy(原OEA)领域实体框架 2.22.2067 发布!> ...
- Rafy 领域实体框架演示(3) - 快速使用 C/S 架构部署
本系列演示如何使用 Rafy 领域实体框架快速转换一个传统的三层应用程序,并展示转换完成后,Rafy 带来的新功能. <福利到!Rafy(原OEA)领域实体框架 2.22.2067 发布!> ...
- Rafy 领域实体框架 - 领域模型设计器(建模工具)设计方案
去年4月,我们为 Rafy 框架添加了领域模型设计器组件.时隔一年,谨以本文,简要说明该领域模型设计器的设计思想. 设计目标 Rafy 实体框架中以领域驱动设计作为指导思想.所以在开发时,以领域建模为 ...
随机推荐
- perl 遍历对象数组
my $appsList ; eval { $appsList = $db->query( $sqlstr1 )->hashes->to_array; }; ### $appsLis ...
- 如何生成报告来枚举出整个sharepoint环境中的每个页面所使用的所有webpart
背景 我的公司的SharePoint环境中购买了大量的第三方webpart,比如Quick Apps, Telerik RadEditor, Nintex Workflow等等..这样做的好处就是成本 ...
- UWP图片编辑器(涂鸦、裁剪、合成)
一.编辑器简介 写这个控件之前总想找一找开源的,可以偷下懒省点事.可是各种地方都搜遍了也没有找到. 于是,那就做第一个吃螃蟹的人吧! 控件主要有三个功能:涂鸦.裁剪.合成. 涂鸦:主要是用到了InkT ...
- 剑指Offer面试题:4.从尾到头打印链表
一.题目:从尾到头打印链表 题目:输入一个链表的头结点,从尾到头反过来打印出每个结点的值. 到解决这个问题肯定要遍历链表.遍历的顺序是从头到尾的顺序,可输出的顺序却是从尾到头.也就是说第一个遍历到的结 ...
- Hadoop学习笔记—10.Shuffle过程那点事儿
一.回顾Reduce阶段三大步骤 在第四篇博文<初识MapReduce>中,我们认识了MapReduce的八大步骤,其中在Reduce阶段总共三个步骤,如下图所示: 其中,Step2.1就 ...
- Module Zero之用户管理
返回<Module Zero学习目录> 用户实体 用户管理者 用户认证 用户实体 用户实体代表应用的一个用户,它派生自AbpUser类,如下所示: public class User : ...
- iOS-----Crash文件分析(一)
开发程序的过程中不管我们已经如何小心,总是会在不经意间遇到程序闪退.脑补一下当你在一群人面前自信的拿着你的App做功能预演的时候,流畅的操作被无情地Crash打断.联想起老罗在发布Smartisan ...
- Java的学习之路
记事本 EditPlus eclipse Java的学习软件,已经系统性学习Java有一段时间了,接下来我想讲一下我在Java学习用到的软件. 1.第一个软件:记事本 记事本是Java学习中最基础的编 ...
- Partition Stats
在分区表中,SQL Server使用一个唯一的分区ID(PartitionID)来标识一个分区,对于任何一个对象(table,index 或 indexed view),都有一个分区号(Prtitio ...
- SQL Server中In-Flight日志究竟是多少
在SQL Server中,利用日志的WAL来保证关系数据库的持久性,但由于硬盘的特性,不可能使得每生成一条日志,就直接向磁盘写一次,因此日志会被缓存起来,到一定数据量才会写入磁盘.这部分已经生 ...