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 領域實體框架發佈!》。(框架目前不開源,但可無償使用。)

相關文章
相關標籤/搜索