這個問題來源於Apworks應用開發框架的設計。由於命令與查詢職責的分離,使得基於CQRS體系結構風格的應用系統的外部儲存系統的結構變得簡單起來:在“命令”部分,簡單地說,只需要 Event Store和Snapshot Store來儲存Domain Model;而“查詢”部分,則又是基於事件派送與偵聽的系統整合。之前我也提到過,“查詢”部分由於不牽涉到Domain Model,於是,它的設計應該隨意性很大:不需要受到Domain Model的牽制,例如,我們可以根據UI所需的資料來源結構進行“查詢”庫的設計。Greg Young在上海閔行企業網站設計與製作他的“CQRS Documents”一文中也提到了這樣一些相關話題:就“查詢”部分而言,雖然也存在“阻抗失衡”效應,但是事件模型與關係模型之間的這種效應,要遠遠小於物件模型與關係模型之間的“阻抗失衡”效應。這是因為,事件模型本身沒有結構,它僅僅表述“該對關係模型做哪些操作”這樣的概念。在設計上,Greg Young建議,採用一種非正規的方式設計“查詢”資料庫,以便儘量減少讀取資料時所需的JOIN操作,比如可以選用基於第一正規化(1NF)的關係模型。這是一種反正規化模型,雖然Greg Young並沒有建議根據UI所需的資料來源結構進行設計,但思想是相同的:基於事件而不拘泥於物件模型本身。由此引申出來的另一個好處就是外部儲存系統架構的隨意性:你可以選用任何儲存技術和儲存媒介,這又給基於系統架構的效能優化提供了便利。因為並非所有的儲存架構都支援“表”、“欄位”、“儲存過程”、“JOIN”這些概念。
根據上面的簡單分析,我們得到一個結論:通常情況下,或許基於CQRS體系結構風格的應用系統更多的是採用的“平整”的外部儲存結構,簡而言之,就是一個資料訪問物件(DAO)對應一張資料表。這也是我所設計的Apworks應用開發框架中預設支援的一種儲存結構。有讀過Apworks原始碼的朋友會發現,在Apworks.Events.Storage名稱空間下,有兩個定製的DAO:DomainEventDataObject,用於表述領域事件的資料結構,以及SnapshotDataObject,用於表述快照的資料結構,與之相對應的就是資料庫中的兩張表:DomainEvents和 Snapshots。雖然結構變得這麼簡單,但是對映關係總還是需要維護的:最簡單的就是需要在物件型別名稱與資料表名之間,以及物件屬性與資料表欄位之間建立起對映關係。在Apworks中,這種對映關係是由Apworks.Storage.IStorageMappingResolver介面完成的。有關這個介面的內容不是本文討論的重點,暫且不深入分析了。
至此,也許你不會接受我上面的討論,認為“基於UI設計資料庫結構”或者“採用1NF、反正規化設計資料庫結構”是無法接受的,那麼,接下來的討論可能對你來說意義也不大了。因為下面的問題是以上面的描述為基礎的:一個資料訪問物件對應一張資料表。不過即使你不認同我的觀點,我也建議你繼續看完本文。
Query Object模式
雖然只是簡單的對映,但畢竟不能忽略這樣的對映關係。Apworks作為一個應用開發框架,需要提供方便的整合介面,以便今後能夠根據不同的客戶需求進行擴充套件。例如在儲存部分,資料的增刪改查(CRUD)是基於資料訪問物件(DAO)的,這樣做的一個好處是能夠對外部儲存系統進行抽象,使得訪問儲存系統的部分能夠無需關係儲存系統的細節問題。客戶有可能選擇SQL Server、Oracle、MySQL等關係型資料庫作為儲存系統,也可以選擇其它的非關係型資料庫作為儲存系統,因此,我們的設計不能僅僅侷限於關係型資料庫,我們需要同時考慮其它形式的資料儲存產品以便將來能夠方便地整合新的儲存方案。假設我們要設計一個針對 DomainEventDataObject的“查詢”功能,我們需要考慮的問題可能會有(但不一定僅限於):
* 需要查詢物件的哪些屬性(或者說與DomainEventDataObject相對應的資料表的哪些欄位)
* 需要根據什麼樣的條件進行查詢
* 查詢是否需要排序
* 是否只查結果集中的任意一條記錄,還是要返回所有的記錄
在Apworks框架的Alpha版本中,查詢的方法定義在Apworks.Storage.IStorage介面中。比如,根據給定的查詢條件和排序方式,對指定DAO進行查詢的方法定義如下:
/// Gets a list of ordered objects from storage by given selection criteria and order.
/// </summary>
/// <typeparam name="T">The type of the object to get.</typeparam>
/// <param name="criteria">The <c>Prope上海閔行企業網站製作rtyBag</c> instance which contains the criteria.</param>
/// <param name="orders">The <c>PropertyBag</c> instance which contains the ordering fields.</param>
/// <param name="sortOrder">The sort order.</param>
/// <returns>A list of ordered objects.</returns>
IEnumerable<T> Select<T>(PropertyBag criteria, PropertyBag orders, SortOrder sortOrder)
where T : class, new();
這個方法是個泛型方法,泛型型別就是DAO的型別。它接受三個引數:前兩個是用於指定查詢條件和排序欄位的PropertyBag,最後一個是指定排序方式的SortOrder。之所以採用PropertyBag,而不是接受SQL字串,原因不僅是因為框架本身需要在今後能夠方便地支援非關係型資料庫,而且更重要的是,雖然SQL已經成為一種業界標準,但實際上不同的關係型資料庫產品對SQL的支援和實現方式也有所不同,有些關係型資料庫產品或許只支援SQL的一些子集,如果單純地把SQL字串作為Select方法的引數,明顯是不合理的。事實上,Apworks.Storage.IStorage實現了Query Object模式[MF, PoEAA],Martin Fowler在他的PoEAA(《企業應用架構模式》)中有以下幾點可以供讀者參考:
* “程式語言是可以支援SQL語句的,但大多數開發人員對此不太熟悉。而且,你需要了解資料庫設計方案以便構造出查詢。可以通過建立特殊的、隱藏了 SQL內部引數化方法的查詢器方法避免這一點。但這樣難以構造更多的特別查詢。而且,如果資料庫設計方案改變,就會需要複製到SQL語句中”
* “查詢物件的一個共同特徵是,它能夠利用記憶體物件的語言而不是用資料庫方案來描述查詢。這意味著我可以使用物件和域名,而不是表和列名。如果物件和資料庫具有相同的結構,這一點就不重要,如果兩者有差異,查詢物件就很有用。為了實現這樣的視角變化,查詢物件需要知道資料庫結構怎樣對映到物件結構,這一功能實際上要用到後設資料對映”【daxnet注:上面提到過,在Apworks框架中,這個後設資料對映的實現,就是 IStorageMappingResolver】
* “為了描述任意的查詢,你需要一個靈活的查詢物件。然而,應用程式經常能用遠少於SQL全部功能的操作來完成這一任務,在此情況下,你的查詢物件就會比較簡單。它不能代表任何東西,但它可以滿足特定的需要。此外,當需要更多功能而進行功能擴充時,通常不會比從零開始建立一個全能的查詢物件更麻煩。因此,應該為當前需求建立一個功能最小化的查詢物件,並隨著需求的增加改進這個查詢物件”
以上三點讓我很有感觸,特別是第三點。目前基於PropertyBag的設計,只能夠支援以AND連線的查詢條件,比如,類似“WHERE a=va AND b=vb AND c=vc…”這樣的查詢,雖然在Apworks Alpha版本中,這樣的查詢已經夠用了,但它不具備擴充套件性,基於關係型資料庫的儲存設計Apworks.Storage.RdbmsStorage已經將這種邏輯寫死了,倘若我們需要一個複雜的查詢,這種方式不僅沒法勝任,而且沒法擴充套件。PropertyBag應該要退休了。
在下一個版本的Apworks中,我使用.NET中的LINQ Expression代替了PropertyBag,並引入了一個WhereClauseBuilder的物件,專門根據LINQ Expression,針對關係型資料庫產生WHERE子句。使用LINQ Expression的好處有:
* LINQ Expression是.NET下的一種查詢標準,多數儲存系統產品能夠提供針對LINQ Expression的查詢解決方案,即使不提供,也可以自己定製Provider,雖然麻煩一點,但總歸是可以實現的
* LINQ Expression能夠完美地“利用記憶體物件的語言而不是用資料庫方案”來描述查詢,語言整合的特性,為開發人員帶來了更多的便捷
* Apworks中,Specification是基於LINQ Expression的,於是,Apworks.Storage.IStorage就能夠實現基於Specification的查詢,實現介面統一
於是技術問題來了:如何將LINQ Expression轉換成WHERE子句,以便Apworks.Storage.IStorage的類(Query Objects)能夠使用這個WHERE子句構造出SQL語句,進而通過ADO.NET直接訪問資料庫?Apworks選用的是Expression Visitor的方案:使用Expression Visitor遍歷表示式樹(Expression Tree)然後產生WHERE子句。在討論Expression Visitor之前,讓我們回顧一下物件結構以及Visitor模式。
Visitor模式
網上面有關Visitor模式的文章太多了,還有相當一部分討論的比較深入透徹,我也就不多說了。總之,Visitor模式在處理較複雜的物件結構時會顯得十分自然:它能夠遍歷結構中的每一個物件,然後針對不同的物件型別作不同的處理。這就看上去像是為這些物件擴充套件了一些方法一樣。之前,我有用過 Visitor模式來驗證程式配置節點的合理性,當節點的型別增加後,只需要擴充套件Visitor即可實現新的驗證邏輯,非常方便。模式歸模式,不同的應用場景,實現方式還是有所不同的。經典的Visitor例子,通常都是利用了函式的過載(多型性),並結合了Composite模式來說明問題,但實際上 Visitor並非一定需要使用函式過載,也不是僅能用在Composite上。Expression Visitor的實現方式,就與這經典的Visitor案例有所不同。
Expression Visitor
在System.Linq.Expressions名稱空間下,有一個ExpressionVisitor的抽象類,我們只需要繼承這個抽象類,並重寫其中的某些Visit方法,即可實現WHERE子句的生成。在這裡我不打算繼續去細究ExpressionVisitor是如何遍歷表示式樹的,我還是描述一下實現WHERE子句生成的幾個細節問題。
1. 支援哪些運算?
LINQ Expression的型別有85種,但並不是SQL中會支援到所有的這85種型別。目前Apworks打算支援常用的條件運算,比如:大於、大於等於、小於、小於等於、不等於、等於這幾種,打算支援常用的邏輯運算:AND、OR、NOT
2. 支援哪些方法(函式)?
目前Apworks支援的方法僅有三種:object.Equals、string.StartsWith和 string.EndsWith。object.Equals將被翻譯成“object = value”,string.StartsWith和string.EndsWith將被翻譯成“LIKE”子句
3. 支援行內函數和變數?
目前僅支援變數,不支援行內函數。
比如:可以用下面的方式來指定Expression:
而不能使用下面的方式來指定Expression:
4. 支援擴充套件?
當然,只需要繼承已有的ExpressionVisitor類,並重寫其中某些方法即可
在當前的Apworks版本中,Apworks.Storage.Builders名稱空間下定義了針對關係型資料庫的 IWhereClauseBuilder介面,以及一個抽象實現:Apworks.Storage.Builders.WhereClauseBuilder類,它不僅實現了IWhereClauseBuilder 介面,同時繼承於System.Linq.Expressions.ExpressionVisitor抽象類,因此,WHERE子句生成的主體邏輯都在這個類中。SqlWhereClauseBuilder類繼承WhereClauseBuilder類,以便實現特定於SQL Server語法的WHERE子句生成器。
由於Apworks.Storage.Builders.WhereClauseBuilder類的原始碼比較長,我就不貼在這裡了,讀者朋友請【點選此處】檢視該類的全部原始碼。
與規約(Specification)整合
在《EntityFramework之領域驅動設計實踐(十):規約模式》一文中,我提出了基於.NET的規約模式的實現方式,為了迎合.NET對LINQ Expression的支援,規約模式的實現也採用了LINQ Expression,而原來的IsSatisfiedfiedBy方法則改為直接使用LINQ Expression來獲得結果:
{
bool IsSatisfiedBy(T obj);
Expression<Func<T, bool>> Expression { get; }
}
public abstract class Specification<T> : ISpecification<T>
{
#region ISpecification Members
public virtual bool IsSatisfiedBy(T obj)
{
return this.Expression.Compile()(obj);
}
public abstract Expression<Func<T, bool>> Expression { get; }
#endregion
}
回過頭來考察Select方法,原本第一個引數是用Expression<Func<T, bool>>型別代替PropertyBag的,現在則可以直接使用ISpecification介面了,於是,我們的Query Object可以使用規約模式來支援資料查詢了。
where T : class, new();
執行過程與客戶端呼叫示例
基於上面的討論,Select方法的定義,已經從使用PropertyBag作為查詢條件,轉變為使用ISpecification介面。注意:orders引數仍然使用PropertyBag,因為目前不打算支援基於表示式的排序條件:
在Apworks.Storage.RdbmsStorage中,使用WhereClauseBuilder.BuildWhereClause方法,根據LINQ Expression生成WHERE子句,進而產生SQL語句並使用ADO.NET訪問關係型資料庫:
where T : class, new()
{
try
{
Expression<Func<T, bool>> expression = null;
WhereClauseBuildResult whereBuildResult = null;
string sql = string.Format("SELECT {0} FROM {1}",
GetFieldNameList<T>(), GetTableName<T>());
if (specification != null)
{
expression = specification.GetExpression();
whereBuildResult = GetWhereClauseBuilder<T>().BuildWhereClause(expression);
sql += " WHERE " + whereBuildResult.WhereClause;
}
if (orders != null && sortOrder != Storage.SortOrder.Unspecified)
{
sql += " ORDER BY " + GetOrderByFieldList<T>(orders);
switch (sortOrder)
{
case Storage.SortOrder.Ascending:
sql += " ASC";
break;
case Storage.SortOrder.Descending:
sql += " DESC";
break;
default: break;
}
}
using (DbCommand command = CreateCommand(sql))
{
if (command.Connection == null)
command.Connection = Connection;
if (Transaction != null)
command.Transaction = Transaction;
if (specification != null)
{
command.Parameters.Clear();
var parameters = GetSelectCriteriaDbParameterList<T>(whereBuildResult.ParameterValues);
foreach (var parameter 上海企業網站設計與製作span>in parameters)
{
command.Parameters.Add(parameter);
}
}
DbDataReader reader = command.ExecuteReader();
List<T> ret = new List<T>();
while (reader.Read())
{
ret.Add(CreateFromReader<T>(reader));
}
reader.Close(); // Very important: reader MUST be closed !!!
return ret;
}
}
catch (ExpressionParseException)
{
throw;
}
catch上海徐匯企業網站設計與製作 style="color: #000000;"> (InfrastructureException)
{
throw;
}
catch (Exception ex)
{
throw ExceptionManager.HandleExceptionAndRethrow<StorageException>(ex,
Resources.EX_SELECT_FROM_STORAGE_FAIL,
typeof(T).AssemblyQualifiedName,
specification != null ? specification.ToString() : "NULL",
orders != null ? orders.ToString() : "NULL",
sortOrder);
}
}
下面這個方法將根據Aggregate Root的型別與ID,返回與之相關的所有Domain Events:
{
try
{
PropertyBag sort = new PropertyBag();
sort.AddSort<long>("Version");
var aggregateRootTypeName = aggregateRootType.AssemblyQualifiedName;
ISpecification<DomainEventDataObject> specification = Specification<DomainEventDataObject>
.Eval(p => p.AggregateRootId == id && p.AggregateRootType == aggregateRootTypeName);<上海網站建設br /> return Select<DomainEventDataObject>(specification, sort, Apworks.Storage.SortOrder.Ascending)
.Select(p => p.ToEntity());
}
catch { throw; }
}