重构 ORM 中的 Sql 生成
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 领域实体框架发布!》。(框架目前不开源,但可免费使用。)
重构 ORM 中的 Sql 生成的更多相关文章
- Rafy 领域实体框架设计 - 重构 ORM 中的 Sql 生成
前言 Rafy 领域实体框架作为一个使用领域驱动设计作为指导思想的开发框架,必然要处理领域实体到数据库表之间的映射,即包含了 ORM 的功能.由于在 09 年最初设计时,ORM 部分的设计并不是最重要 ...
- 在powerDesigner中通过SQL生成pdm
在项目需求分析的阶段,通常需要画数据库表的pdm图.有时候会直接画pdm来设计表,有时候是通过其他方式,如用纸和笔去画……当数据库中的表已经建立好了,怎么把数据库中的表导成SQL形式,然后生成pdm图 ...
- c#保存datagridview中的数据时报错 “动态SQL生成失败。找不到关键信息”
ilovejinglei 原文 C#中保存datagridview中的数据时报错"动态SQL生成失败.找不到关键信息" 问题描述 相关代码 using System; us ...
- sql 中获取最后生成的标识值 IDENT_CURRENT ,@@IDENTITY ,SCOPE_IDENTITY 的用法和区别
原文:sql 中获取最后生成的标识值 IDENT_CURRENT ,@@IDENTITY ,SCOPE_IDENTITY 的用法和区别 IDENT_CURRENT 返回为任何会话和任何作用域中的指定表 ...
- Django ORM 中的批量操作
Django ORM 中的批量操作 在Hibenate中,通过批量提交SQL操作,部分地实现了数据库的批量操作.但在Django的ORM中的批量操作却要完美得多,真是一个惊喜. 数据模型定义 首先,定 ...
- bbs项目学习到的知识点(orm中的extra)
注册 form组件给input 的标签 添加样式类 参见这篇博客(点击) 上传图像 1.解决 一点击图像就会直接打开上传文件的按钮 #这儿利用了 label标签和input的特殊的联动功能 < ...
- Django ORM中常用字段和参数
一些说明: 表myapp_person的名称是自动生成的,如果你要自定义表名,需要在model的Meta类中指定 db_table 参数,强烈建议使用小写表名,特别是使用MySQL作为后端数据库时. ...
- {Django基础六之ORM中的锁和事务}一 锁 二 事务
Django基础六之ORM中的锁和事务 本节目录 一 锁 二 事务 一 锁 行级锁 select_for_update(nowait=False, skip_locked=False) #注意必须用在 ...
- ORM中的N+1问题
在orm中有一个经典的问题,那就是N+1问题,比如hibernate就有这个问题,这一般都是不可避免的. [N+1问题是怎么出现的] N+1一般出现在一对多查询中,下面以Group和User为例,Gr ...
随机推荐
- Chromium Graphics: GPUclient的原理和实现分析之间的同步机制-Part I
摘要:Chromium于GPU多个流程架构的同意GPUclient这将是这次访问的同时GPU维修,和GPUclient这之间可能存在数据依赖性.因此必须提供一个同步机制,以确保GPU订购业务.本文讨论 ...
- [Network]Introduction and Basic concepts
[该系列是检讨计算机网络知识.因为现在你想申请出国.因此,在写这篇博客系列的大多数英语.虽然英语,但大多数就是我自己的感受和理解,供大家学习和讨论起来] 1 Network Edge The devi ...
- jQuery中的.height()、.innerHeight()和.outerHeight()
jQuery中的.height()..innerHeight()和.outerHeight()和W3C的盒模型相关的几个获取元素尺寸的方法.对应的宽度获取方法分别为.width()..innerWid ...
- JS如何判断包括IE11在内的IE浏览器
原文:JS如何判断包括IE11在内的IE浏览器 今天碰到一个奇怪的问题,有一个页面,想指定用IE浏览器打开,在VS开发环境没有问题,但部署到服务器上,即使是用IE打开页面,还是提示"仅支持I ...
- Huffman树与最优二叉树续
OK,昨天我们对huffman数的基本知识,以及huffman树的创建做了一些简介,http://www.cnblogs.com/Frank-C/p/5017430.html 今天接着聊: huffm ...
- linux_vim_快捷键
1.vim ~/.vimrc 进入配置文件 如果不知道vimrc文件在哪,可使用 :scriptnames 来查看 set nu #行号 set tabstop=4 #一个tab为4个空格长度 set ...
- elasticsearch的rest搜索--- 总述
目录: 一.针对这次装B 的解释 二.下载,安装插件elasticsearch-1.7.0 三.索引的mapping 四. 查询 五.对于相关度的大牛的文档 一.针对这次装B 的解释 因为现在又 ...
- Advance Installer安装问题
一,在Advance Installer中注冊dll 1,首先将文件加入到Files And Folders中.此处以InstallValidate.dll为例. 2,在Custom Action处进 ...
- sql事务,在sql2000里判断执行是否成功用@@ERROR 判断
原文:sql事务,在sql2000里判断执行是否成功用@@ERROR 判断 贴个sql事务,在sql2000里判断执行是否成功用@@ERROR 判断 这个东西多少还是有点问题,sql2005了可以用t ...
- 【百度地图API】如何用圆形搜索获取中心点周围100米内全部关键点?如天安门附近所有的餐厅、加油站、宾馆、大厦等
原文:[百度地图API]如何用圆形搜索获取中心点周围100米内全部关键点?如天安门附近所有的餐厅.加油站.宾馆.大厦等 摘要: 在LBS上有这样一个常用的功能,查找附近所有的关键点(POI点,比如标志 ...