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 领域实体框架发布!》。(框架目前不开源,但可免费使用。)

 

欢迎转载,转载请注明:

转载自 胡庆访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. Git 命令速查表

    Git 命令速查表 1.常用的Git命令 命令 简要说明 git add 添加至暂存区 git add-interactive 交互式添加 git apply 应用补丁 git am 应用邮件格式补丁 ...

  2. Kd-Tree算法原理和开源实现代码

    本文介绍一种用于高维空间中的高速近期邻和近似近期邻查找技术--Kd-Tree(Kd树). Kd-Tree,即K-dimensional tree,是一种高维索引树形数据结构,经常使用于在大规模的高维数 ...

  3. 第2章 简单工厂模式(Sample Factory)

    原文 第2章 简单工厂模式(Sample Factory) 一般用到的场景:对象多次被实例引用,切有可能会发生变化 拿我们的简单三层举例子 先定义dal层 1 2 3 4 5 6 7 8     cl ...

  4. 第20章 状态模式(State Pattern)

    原文 第20章 状态模式(State Pattern) 状态模式  概述:   当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类. 状态模式主要解决的是当控制一个对象状态的条件表 ...

  5. 【spring教程之二】spring注射剂xml构造方法参数

    1.上述续,假设你想注入bean当文件,传递给构造函数的参数.主要的变化是需要spring.xml配置文件来配置. <?xml version="1.0" encoding= ...

  6. NYoj WAJUEJI which home strong!(简单搜索)

    题目链接:http://acm.nyist.edu.cn/JudgeOnline/problem.php?pid=1100 这道题,自己初写搜索,给学长气的只打我,Orz....... 搜索的思路要理 ...

  7. 浅谈JavaScript中typeof与instanceof的区别

      首先,我们从其常规定义入手:       instanceof 运算符可以用来判断某个构造函数的 prototype 属性是否存在另外一个要检测对象的原型链上.(需要注意的一点是:prototyp ...

  8. jquery 元素控制(附加元素/其他内容)引进和应用

    一个.在内部元素/外部附加元件 append,prepend:加入到该子元素  before,after:元素加入 html: <div id="content"> 在 ...

  9. 私人定制javascript事件处理机制(浅谈)

    看到园子里关于事件监听发表的文章,我都有点不好意思写了.不过想想我的题目以私人定制作开头也就妥妥地写吧. 事件相关概念 1.事件类型 发生事件的字符串 有传统事件类型 比如表单.window事件等 D ...

  10. openSUSE 安装

    https://lug.ustc.edu.cn/sites/opensuse-guide/installation.php 开始 1. 简介2. 改用 GNU/Linux3. 获取 openSUSE4 ...