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