前言


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 生成的更多相关文章

  1. 重构 ORM 中的 Sql 生成

    Rafy 领域实体框架设计 - 重构 ORM 中的 Sql 生成   前言 Rafy 领域实体框架作为一个使用领域驱动设计作为指导思想的开发框架,必然要处理领域实体到数据库表之间的映射,即包含了 OR ...

  2. Rafy 领域实体框架 - 树型实体功能(自关联表)

      在 Rafy 领域实体框架中,对自关联的实体结构做了特殊的处理,下面对这一功能进行讲解. 场景 在开发数据库应用程序时,往往会遇到自关联表的场景.例如,分类信息.组织架构中的部门.文件夹信息等,都 ...

  3. Rafy 领域实体框架简介

    按照最新的功能,更新了最新版的<Rafy 领域实体框架的介绍>,内容如下: 本文包含以下章节: 简介 特点 优势 简介 Rafy 领域实体框架是一个轻量级 ORM 框架. 与一般的 ORM ...

  4. Rafy 领域实体框架 - 公司内部培训视频

    本月给公司内部一个项目做架构重构,其中使用到了 Rafy 框架.所以我培训了 Rafy 领域实体框架的使用方法,过程中录制了视频,方便其他同事查看.现在把视频放到园里来分享下,有兴趣的朋友可以看看,有 ...

  5. Rafy 领域实体框架示例(1) - 转换传统三层应用程序

    Rafy 领域实体框架发布后,虽然有帮助文档,许多朋友还是反映学习起来比较复杂,希望能开发一个示例程序,展示如何使用 Rafy 领域实体框架所以,本文通过使用 Rafy 领域实体框架来改造一个传统的三 ...

  6. Rafy 领域实体框架演示(2) - 新功能展示

    本文的演示需要先完成上一篇文章中的演示:<Rafy 领域实体框架示例(1) - 转换传统三层应用程序>.在完成改造传统的三层系统之后,本文将讲解使用 Rafy 实体框架后带来的一些常用功能 ...

  7. Rafy 领域实体框架演示(4) - 使用本地文件型数据库 SQLCE 绿色部署

    本系列演示如何使用 Rafy 领域实体框架快速转换一个传统的三层应用程序,并展示转换完成后,Rafy 带来的新功能. <福利到!Rafy(原OEA)领域实体框架 2.22.2067 发布!> ...

  8. Rafy 领域实体框架演示(3) - 快速使用 C/S 架构部署

    本系列演示如何使用 Rafy 领域实体框架快速转换一个传统的三层应用程序,并展示转换完成后,Rafy 带来的新功能. <福利到!Rafy(原OEA)领域实体框架 2.22.2067 发布!> ...

  9. Rafy 领域实体框架 - 领域模型设计器(建模工具)设计方案

    去年4月,我们为 Rafy 框架添加了领域模型设计器组件.时隔一年,谨以本文,简要说明该领域模型设计器的设计思想. 设计目标 Rafy 实体框架中以领域驱动设计作为指导思想.所以在开发时,以领域建模为 ...

随机推荐

  1. JSP学习笔记

    JSP学习笔记 Jsp网页主要分为Elements与Template Data两部分. Template Data:JSP Container不处理的部分,例如HTML内容 Elements:必须经由 ...

  2. 开放式管理基础结构 OMI

    Windows 长久以来在 CIM 实施领域一直傲立桥头,而这一切都是从 WMI(Windows 管理基础结构)开始的.分布式管理任务组 (DMTF) 通用信息模型 (CIM) 是一种开放式标准,用于 ...

  3. 体验 ASP.NET Core 1.1 中预编译 MVC Razor 视图

    这是从 ASP.NET Core 1.1 官方发布博文中学到的一招,可以在 dontet publish 时将 Razor 视图编译为 .dll 文件. 需要在 project.json 中添加如下配 ...

  4. 剑指Offer面试题:26.字符串的排列

    一.题目:字符串的排列 题目:输入一个字符串,打印出该字符串中字符的所有排列.例如输入字符串abc,则打印出由字符a.b.c所能排列出来的所有字符串abc.acb.bac.bca.cab和cba. 二 ...

  5. .NET 基础 一步步 一幕幕 [.NET 简介]

    .NET 简介 .NET是 Microsoft XML Web services 平台.是微软用来实现XML,Web Services,SOA(面向服务的体系结构service-oriented ar ...

  6. Apk去掉签名以及重新签名的方法

    Android开发中很重要的一部就是用自己的密钥给Apk文件签名,不经过签名的Apk文件一般是无法安装的,就算装了最后也是失败. 网上流传的"勾选允许安装未知来源的应用"其实跟签不 ...

  7. Angular使用$compile为从Ajax加载的HTML绑定ng-click事件

    这是一个Angular使用$compile为从Ajax加载的HTML绑定ng-click事件的实现方式,由于近期忙碌,就先放代码.代码如下: <table data-ng-table=" ...

  8. jQuery 2.0.3 源码分析 事件绑定 - bind/live/delegate/on

    事件(Event)是JavaScript应用跳动的心脏,通过使用JavaScript ,你可以监听特定事件的发生,并规定让某些事件发生以对这些事件做出响应 事件的基础就不重复讲解了,本来是定位源码分析 ...

  9. android ANR产生原因和解决办法

    转自http://blog.sina.com.cn/s/blog_618199e60101kvbl.html ANR (Application Not Responding) ANR定义:在Andro ...

  10. javascript之一切皆为对象3

    在前面两个章节“Javascript之一切皆为对象1”和“Javascript之一切皆为对象2”中,曾提到: 1.“一切(引用类型)皆为对象” 2. “每个函数都有一个prototype” 3. “每 ...