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. 1: /// <summary>
  1. 2: /// 使用托管属性进行查询的条件封装。
  1. 3: /// </summary>
  1. 4: public interface IPropertyQuery : IDirectlyConstrain
  1. 5: {
  1. 6: /// <summary>
  1. 7: /// 是否还没有任何语句
  1. 8: /// </summary>
  1. 9: bool IsEmpty { get; }
  1. 10:  
  1. 11: /// <summary>
  1. 12: /// 当前的查询是一个分页查询,并使用这个对象来描述分页的信息。
  1. 13: /// </summary>
  1. 14: PagingInfo PagingInfo { get; }
  1. 15:  
  1. 16: /// <summary>
  1. 17: /// 用于查询的 Where 条件。
  1. 18: /// </summary>
  1. 19: IConstraintGroup Where { get; set; }
  1. 20:  
  1. 21: /// <summary>
  1. 22: /// 对引用属性指定的表使用关联查询
  1. 23: ///
  1. 24: /// 调用此语句会生成相应的 INNER JOIN 语句,并把所有关联的数据在 SELECT 中加上。
  1. 25: ///
  1. 26: /// 注意!!!
  1. 27: /// 目前不支持同时 Join 两个不同的引用属性,它们都引用同一个实体/表。
  1. 28: /// </summary>
  1. 29: /// <param name="property"></param>
  1. 30: /// <param name="type">是否同时查询出相关的实体数据。</param>
  1. 31: /// <param name="propertyOwner">
  1. 32: /// 显式指定该引用属性对应的拥有类型。
  1. 33: /// 一般使用在以下情况中:当引用属性定义在基类中,而当前正在对子类进行查询时。
  1. 34: /// </param>
  1. 35: /// <returns></returns>
  1. 36: IPropertyQuery JoinRef(IRefProperty property, JoinRefType type = JoinRefType.JoinOnly, Type propertyOwner = null);
  1. 37:  
  1. 38: /// <summary>
  1. 39: /// 按照某个属性排序。
  1. 40: ///
  1. 41: /// 可以调用此方法多次来指定排序的优先级。
  1. 42: /// </summary>
  1. 43: /// <param name="property">按照此属性排序</param>
  1. 44: /// <param name="direction">排序方向。</param>
  1. 45: /// <returns></returns>
  1. 46: IPropertyQuery OrderBy(IManagedProperty property, OrderDirection direction);
  1. 47:  
  1. 48: //其它部分省略...
  1. 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. 1: /// <summary>
  1. 2: /// 表示一个 Sql 查询语句。
  1. 3: /// </summary>
  1. 4: class SqlSelect : SqlNode
  1. 5: {
  1. 6: private IList _orderBy;
  1. 7:  
  1. 8: public override SqlNodeType NodeType
  1. 9: {
  1. 10: get { return SqlNodeType.SqlSelect; }
  1. 11: }
  1. 12:  
  1. 13: /// <summary>
  1. 14: /// 是否只查询数据的条数。
  1. 15: ///
  1. 16: /// 如果这个属性为真,那么不再需要使用 Selection。
  1. 17: /// </summary>
  1. 18: public bool IsCounting { get; set; }
  1. 19:  
  1. 20: /// <summary>
  1. 21: /// 是否需要查询不同的结果。
  1. 22: /// </summary>
  1. 23: public bool IsDistinct { get; set; }
  1. 24:  
  1. 25: /// <summary>
  1. 26: /// 如果指定此属性,表示需要查询的条数。
  1. 27: /// </summary>
  1. 28: public int? Top { get; set; }
  1. 29:  
  1. 30: /// <summary>
  1. 31: /// 要查询的内容。
  1. 32: /// 如果本属性为空,表示要查询所有列。
  1. 33: /// </summary>
  1. 34: public SqlNode Selection { get; set; }
  1. 35:  
  1. 36: /// <summary>
  1. 37: /// 要查询的数据源。
  1. 38: /// </summary>
  1. 39: public SqlSource From { get; set; }
  1. 40:  
  1. 41: /// <summary>
  1. 42: /// 查询的过滤条件。
  1. 43: /// </summary>
  1. 44: public SqlConstraint Where { get; set; }
  1. 45:  
  1. 46: /// <summary>
  1. 47: /// 查询的排序规则。
  1. 48: /// 可以指定多个排序条件,其中每一项都必须是一个 SqlOrderBy 对象。
  1. 49: /// </summary>
  1. 50: public IList OrderBy
  1. 51: {
  1. 52: get
  1. 53: {
  1. 54: if (_orderBy == null)
  1. 55: {
  1. 56: _orderBy = new ArrayList();
  1. 57: }
  1. 58: return _orderBy;
  1. 59: }
  1. 60: internal set { _orderBy = value; }
  1. 61: }
  1. 62:  
  1. 63: //...
  1. 64: }

Sql 生成器

 

使用访问器模式设计,用于遍历整个 Sql 语法树。以下是 SqlNodeVisitor 的代码:

  1. 1: /// <summary>
  1. 2: /// SqlNode 语法树的访问器
  1. 3: /// </summary>
  1. 4: abstract class SqlNodeVisitor
  1. 5: {
  1. 6: protected SqlNode Visit(SqlNode node)
  1. 7: {
  1. 8: switch (node.NodeType)
  1. 9: {
  1. 10: case SqlNodeType.SqlLiteral:
  1. 11: return this.VisitSqlLiteral(node as SqlLiteral);
  1. 12: case SqlNodeType.SqlSelect:
  1. 13: return this.VisitSqlSelect(node as SqlSelect);
  1. 14: case SqlNodeType.SqlColumn:
  1. 15: return this.VisitSqlColumn(node as SqlColumn);
  1. 16: case SqlNodeType.SqlTable:
  1. 17: return this.VisitSqlTable(node as SqlTable);
  1. 18: case SqlNodeType.SqlColumnConstraint:
  1. 19: return this.VisitSqlColumnConstraint(node as SqlColumnConstraint);
  1. 20: case SqlNodeType.SqlBinaryConstraint:
  1. 21: return this.VisitSqlBinaryConstraint(node as SqlBinaryConstraint);
  1. 22: case SqlNodeType.SqlJoin:
  1. 23: return this.VisitSqlJoin(node as SqlJoin);
  1. 24: case SqlNodeType.SqlArray:
  1. 25: return this.VisitSqlArray(node as SqlArray);
  1. 26: case SqlNodeType.SqlSelectAll:
  1. 27: return this.VisitSqlSelectAll(node as SqlSelectAll);
  1. 28: case SqlNodeType.SqlColumnsComparisonConstraint:
  1. 29: return this.VisitSqlColumnsComparisonConstraint(node as SqlColumnsComparisonConstraint);
  1. 30: case SqlNodeType.SqlExistsConstraint:
  1. 31: return this.VisitSqlExistsConstraint(node as SqlExistsConstraint);
  1. 32: case SqlNodeType.SqlNotConstraint:
  1. 33: return this.VisitSqlNotConstraint(node as SqlNotConstraint);
  1. 34: case SqlNodeType.SqlSubSelect:
  1. 35: return this.VisitSqlSubSelect(node as SqlSubSelect);
  1. 36: default:
  1. 37: break;
  1. 38: }
  1. 39: throw new NotImplementedException();
  1. 40: }
  1. 41:  
  1. 42: protected virtual SqlJoin VisitSqlJoin(SqlJoin sqlJoin)
  1. 43: {
  1. 44: this.Visit(sqlJoin.Left);
  1. 45: this.Visit(sqlJoin.Right);
  1. 46: this.Visit(sqlJoin.Condition);
  1. 47: return sqlJoin;
  1. 48: }
  1. 49:  
  1. 50: protected virtual SqlBinaryConstraint VisitSqlBinaryConstraint(SqlBinaryConstraint node)
  1. 51: {
  1. 52: this.Visit(node.Left);
  1. 53: this.Visit(node.Right);
  1. 54: return node;
  1. 55: }
  1. 56:  
  1. 57: //...
  1. 58: }

基于实体的查询

1. IQuery 相关接口用于描述整个基于实体的查询。

例如,IColumnNode 表示一个列节点,其实是由一个实体属性来指定的:

  1. 1: namespace Rafy.Domain.ORM.Query
  1. 2: {
  1. 3: /// <summary>
  1. 4: /// 一个列节点
  1. 5: /// </summary>
  1. 6: public interface IColumnNode : IQueryNode
  1. 7: {
  1. 8: /// <summary>
  1. 9: /// 本列属于指定的数据源
  1. 10: /// </summary>
  1. 11: INamedSource Owner { get; set; }
  1. 12:  
  1. 13: /// <summary>
  1. 14: /// 本属性对应一个实体的托管属性
  1. 15: /// </summary>
  1. 16: IManagedProperty Property { get; set; }
  1. 17:  
  1. 18: /// <summary>
  1. 19: /// 本属性在查询结果中使用的别名。
  1. 20: /// </summary>
  1. 21: string Alias { get; set; }
  1. 22: }
  1. 23: }

2. EntityQuery 层中的类型实现了 IQuery 中对应的接口,并使用领域实体的相关 API 来实现从实体到表、实体属性到列的转换。同时,为了减少对象的数量,这些类型与 Sql 语法树的关系都使用继承,而不是关联。也就是说,它们直接从 SqlTree 对应的类型上继承下来,这样,在构造 EntityQuery 的同时,也构造好了底层的 Sql 语法树。

3. QueryFactory 封装了大量易用的 API 来构造 IQuery 接口。

使用示例


下面,就以几个典型的单元测试的相关代码来说明新的查询框架的使用方法:

使用 Linq 的数据层查询

  1. 1: public int LinqCountByBookName(string name)
  1. 2: {
  1. 3: return this.FetchCount(r => r.DA_LinqCountByBookName(name));
  1. 4: }
  1. 5: private EntityList DA_LinqCountByBookName(string name)
  1. 6: {
  1. 7: var q = this.CreateLinqQuery();
  1. 8: q = q.Where(c => c.Book.Name == name);
  1. 9: return this.QueryList(q);
  1. 10: }

使用 IQuery 的数据层查询

  1. 1: public int CountByBookName2(string name)
  1. 2: {
  1. 3: return this.FetchCount(r => r.DA_CountByBookName2(name));
  1. 4: }
  1. 5: private EntityList DA_CountByBookName2(string name)
  1. 6: {
  1. 7: var source = f.Table(this);
  1. 8: var bookSource = f.Table<BookRepository>();
  1. 9: var q = f.Query(
  1. 10: from: f.Join(source, bookSource)
  1. 11: );
  1. 12: q.AddConstraintIf(Book.NameProperty, PropertyOperator.Equal, name);
  1. 13: return this.QueryList(q);
  1. 14: }

可以看到,使用 IQuery 接口来查询,虽然灵活性最大、性能更好,但是相对于 Linq 来说会更加复杂。

使用 IQuery 来生成 Sql

  1. 1: [TestMethod]
  1. 2: public void ORM_TableQuery_InSubSelect()
  1. 3: {
  1. 4: var f = QueryFactory.Instance;
  1. 5: var articleSource = f.Table(RF.Concrete<ArticleRepository>());
  1. 6: var userSource = f.Table(RF.Concrete<BlogUserRepository>());
  1. 7: var query = f.Query(
  1. 8: from: userSource,
  1. 9: where: f.Constraint(
  1. 10: column: userSource.Column(BlogUser.IdProperty),
  1. 11: op: PropertyOperator.In,
  1. 12: value: f.Query(
  1. 13: selection: articleSource.Column(Article.UserIdProperty),
  1. 14: from: articleSource,
  1. 15: where: f.Constraint(articleSource.Column(Article.CreateDateProperty), DateTime.Today)
  1. 16: )
  1. 17: )
  1. 18: );
  1. 19:  
  1. 20: var generator = new SqlServerSqlGenerator { AutoQuota = false };
  1. 21: f.Generate(generator, query);
  1. 22: var sql = generator.Sql;
  1. 23:  
  1. 24: Assert.IsTrue(sql.ToString() ==
  1. 25: @"SELECT *
  1. 26: FROM BlogUser
  1. 27: WHERE BlogUser.Id IN (
  1. 28: SELECT Article.UserId
  1. 29: FROM Article
  1. 30: WHERE Article.CreateDate = {0}
  1. 31: )");
  1. 32: Assert.IsTrue(sql.Parameters.Count == 1);
  1. 33: Assert.IsTrue(sql.Parameters[0].Equals(DateTime.Today));
  1. 34: }

使用 SqlTree 来生成 Sql

  1. 1: [TestMethod]
  1. 2: public void ORM_SqlTree_Select_InSubSelect()
  1. 3: {
  1. 4: var select = new SqlSelect();
  1. 5: var articleTable = new SqlTable { TableName = "Article" };
  1. 6: var subSelect = new SqlSelect
  1. 7: {
  1. 8: Selection = new SqlColumn { Table = articleTable, ColumnName = "UserId" },
  1. 9: From = articleTable,
  1. 10: Where = new SqlColumnConstraint
  1. 11: {
  1. 12: Column = new SqlColumn { Table = articleTable, ColumnName = "CreateDate" },
  1. 13: Operator = SqlColumnConstraintOperator.Equal,
  1. 14: Value = DateTime.Today
  1. 15: }
  1. 16: };
  1. 17:  
  1. 18: var userTable = new SqlTable { TableName = "User" };
  1. 19: select.Selection = new SqlSelectAll();
  1. 20: select.From = userTable;
  1. 21: select.Where = new SqlColumnConstraint
  1. 22: {
  1. 23: Column = new SqlColumn { Table = userTable, ColumnName = "Id" },
  1. 24: Operator = SqlColumnConstraintOperator.In,
  1. 25: Value = subSelect
  1. 26: };
  1. 27:  
  1. 28: var generator = new SqlServerSqlGenerator { AutoQuota = false };
  1. 29: generator.Generate(select);
  1. 30: var sql = generator.Sql;
  1. 31: Assert.IsTrue(sql.ToString() == @"SELECT *
  1. 32: FROM User
  1. 33: WHERE User.Id IN (
  1. 34: SELECT Article.UserId
  1. 35: FROM Article
  1. 36: WHERE Article.CreateDate = {0}
  1. 37: )");
  1. 38: Assert.IsTrue(sql.Parameters.Count == 1);
  1. 39: Assert.IsTrue(sql.Parameters[0].Equals(DateTime.Today));
  1. 40: }

框架下载


框架使用测试驱动的方法开发,在开发时是先编写相关的测试用例,再实现内部代码。重构的同时,我们为能想到的场景都编写了测试用例:

目前,框架版本也升级到了 2.23.2155。

有兴趣的同学,了解、下载最新的框架,请参考:《Rafy 领域实体框架发布!》。(框架目前不开源,但可免费使用。)

 

欢迎转载,转载请注明:

转载自 胡庆访http://zgynhqf.cnblogs.com/ ]

 

 
 

重构 ORM 中的 Sql 生成的更多相关文章

  1. Rafy 领域实体框架设计 - 重构 ORM 中的 Sql 生成

    前言 Rafy 领域实体框架作为一个使用领域驱动设计作为指导思想的开发框架,必然要处理领域实体到数据库表之间的映射,即包含了 ORM 的功能.由于在 09 年最初设计时,ORM 部分的设计并不是最重要 ...

  2. 在powerDesigner中通过SQL生成pdm

    在项目需求分析的阶段,通常需要画数据库表的pdm图.有时候会直接画pdm来设计表,有时候是通过其他方式,如用纸和笔去画……当数据库中的表已经建立好了,怎么把数据库中的表导成SQL形式,然后生成pdm图 ...

  3. c#保存datagridview中的数据时报错 “动态SQL生成失败。找不到关键信息”

    ilovejinglei 原文 C#中保存datagridview中的数据时报错"动态SQL生成失败.找不到关键信息" 问题描述     相关代码 using System; us ...

  4. sql 中获取最后生成的标识值 IDENT_CURRENT ,@@IDENTITY ,SCOPE_IDENTITY 的用法和区别

    原文:sql 中获取最后生成的标识值 IDENT_CURRENT ,@@IDENTITY ,SCOPE_IDENTITY 的用法和区别 IDENT_CURRENT 返回为任何会话和任何作用域中的指定表 ...

  5. Django ORM 中的批量操作

    Django ORM 中的批量操作 在Hibenate中,通过批量提交SQL操作,部分地实现了数据库的批量操作.但在Django的ORM中的批量操作却要完美得多,真是一个惊喜. 数据模型定义 首先,定 ...

  6. bbs项目学习到的知识点(orm中的extra)

    注册 form组件给input 的标签 添加样式类  参见这篇博客(点击) 上传图像 1.解决 一点击图像就会直接打开上传文件的按钮 #这儿利用了 label标签和input的特殊的联动功能 < ...

  7. Django ORM中常用字段和参数

    一些说明: 表myapp_person的名称是自动生成的,如果你要自定义表名,需要在model的Meta类中指定 db_table 参数,强烈建议使用小写表名,特别是使用MySQL作为后端数据库时. ...

  8. {Django基础六之ORM中的锁和事务}一 锁 二 事务

    Django基础六之ORM中的锁和事务 本节目录 一 锁 二 事务 一 锁 行级锁 select_for_update(nowait=False, skip_locked=False) #注意必须用在 ...

  9. ORM中的N+1问题

    在orm中有一个经典的问题,那就是N+1问题,比如hibernate就有这个问题,这一般都是不可避免的. [N+1问题是怎么出现的] N+1一般出现在一对多查询中,下面以Group和User为例,Gr ...

随机推荐

  1. thoughtworks笔试整理

    笔试了,时间1个半小时.没想到居然有7/10是开放性问题.大意例如以下:1.为什么选择增加ThoughtWorks.200字以内,不能用"interesting"."ch ...

  2. 采购申请 POCIRM-001:ORA-01403: 无论数据未找到

    今天就让同事帮忙看问题.当请求生成采购订单,在销售模块错误提交销售订单 查看请求日志 +-------------------------------------------------------- ...

  3. 使用sqlnet.ora限制IP访问

    他在最后一个超级遭遇了许多方法值,然后找到一个方法,在DB上限IP访问. http://blog.csdn.net/jacson_bai/article/details/18097805 ENV:   ...

  4. linux高级技巧:rsync同步(一个)

    1.rsync基本介绍         rsync这是Unix下的一款应用软件,它能同步更新两处计算机的文件与文件夹,并适当利用差分编码以降低数据传输.rsync中一项与其它大部分类似程序或协议中所未 ...

  5. [CLR via C#]1.5 本地代码生成器:NGen.exe

    原文:[CLR via C#]1.5 本地代码生成器:NGen.exe 1. NGen.exe工具,可以在一个程序安装到用户计算机时,将IL代码编译成为本地代码.由于代码在安装时已经编译好,所以CLR ...

  6. c语言复杂声明解析

    这是个好东西,接触c语言好几年了,第一次看到这东西,惊喜万分. 先提供个分析案例,以后看方便 vector <int> * (*seq_array[]) (int )={func1,fun ...

  7. javascript如何解析json对javascript如何解析json对象并动态赋值到select列表象并动态赋值到select列表

    原文 javascript如何解析json对象并动态赋值到select列表 JSON(JavaScriptObject Notation)一种简单的数据格式,比xml更轻巧.JSON是JavaScri ...

  8. MapGuide应用程序演示样例——你好,MapGuide!

    图 3‑4显示了基于MapGuide的Web应用程序的开发流程,整个开发流程能够分为五个阶段.图中,矩形代表任务,椭圆形被任务使用的或被任务创建的实体,箭头代表数据流. 1) 载入文件类型的数据,配置 ...

  9. android学习8(ListView高级使用)

    ListView在android更开放的,于是继续ListView说明使用. 首先创建一个android项目,项目名为ListViewTest. ListView的简单使用 改动布局文件,改动后代码例 ...

  10. android 使用asm.jar将android手机屏幕投射到电脑

    使用asm.jar将Android手机到电脑屏幕投影 有时候可能须要将手机上的一些操作投影出来,比方一些App Demo的展示等.事实上,有专门的硬件设备能干这件事儿.但不是必需专门为展示个Demo去 ...