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

前言


Rafy 领域实体框架做为一个使用领域驱动设计做为指导思想的开发框架,必然要处理领域实体到数据库表之间的映射,即包含了 ORM 的功能。因为在 09 年最初设计时,ORM 部分的设计并非最重要的部分,那里 Rafy 的核心是产品线工程、模型驱动开发、界面生成等。因此当时,咱们简单地采用了一个开源的小型 ORM 框架:《Lite ORM Library》。这个 ORM 框架能够生成比较简单的 Sql 语句,以处理通常性的状况。html

随着不断使用,咱们也不断对 ORM 的源码作了很多改动,让它在支持简单语句生成的同时,也支持让开发人员直接使用手动编写的 Sql 语句来查询领域实体。可是过程当中,一直没有修改最核心的 Sql 语句生成模块。随着应用的不断深刻,遇到的场景愈来愈多,须要生成复杂 Sql 语句的场景也愈来愈多。而这些场景若是还让开发人员本身去编写复杂 Sql 语句,不但框架的易用性降低,并且因为写了过多的 Sql 语句,还会让开发人员面向领域实体来开发的思想减弱。node

这两周,咱们对 Sql 语句生成模块实施了重构。与其说是重构,不如说重写,由于 90% Lite ORM 的类库都已经再也不使用。可是又不得不面对对历史代码中接口的兼容性问题。接下来,将说明本次重构中的关键技术点。sql

 

旧代码讲解


最初采用的 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 链接的条件。
imageide

能够看到,虽然有 SqlWhereConstraint 来添加任意的 Sql 语句做为 Where 约束条件,可是这样的结构仍是比较简单,不足以描述全部的 Sql。性能

 

重构方案


咱们的目标是实现复杂 Sql 的生成,理论上须要支持全部能想到的 Sql 语句的生成。单元测试

初期方案其实很简单,就是使用解释器模式与访问器模式配合来重构底层代码。根据 Sql 的语法规定,构造 Sql 语法树节点中的相关类型,这样就能够用一棵树来解释任意的 Sql 语句;同时使用访问器模式来遍历某个具体 Sql 语法树。过程当中还须要特别注意,尽可能不要构造没必要要的树节点,以增长垃圾回收器的压力。测试

在此初步方案上,还须要考虑:分层架构、组件间依赖、以及旧代码的兼容性设计。

如下是整个方案的分层设计:

image

SqlTree:核心的、可重用的 Sql 语法树层。定义了通用的 Sql 语法结构,并解决从语法树到 Sql 语句的转换、生成,以及屏蔽不一样数据库间不一样子句的生成规则。

EntityQuery:把 SqlTree 做为类库引用,同时整合领域实体、实体属性的设计。

Query Interface:以 IQuery 接口的方式提供给应用层。

Linq Query:为了给开发人员提供更易用的接口,须要提供 Linq 语法的支持。本层用于解析 Linq 表达式树,并生成最终的实体查询的对象。

Property Query:为了兼容旧的接口,该部分在提供旧接口的前提下,换为使用新的 IQuery 来实现。

Application:开发人员的应用层代码。可使用最易用的 Linq、旧的 PropertyQuery,同时也能够直接使用 IQuery 接口来完成复杂查询。

 

组件详细设计


Sql 语法树

image image

使用解释器模式设计,用于描述 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 生成器

image image

使用访问器模式设计,用于遍历整个 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:  }

 

基于实体的查询

image

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

image

例如,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:  }

 

框架下载


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

imageimageimage

 

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

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

相关文章
相关标签/搜索