淺談Apworks對MongoDB的支持:設計與實現


概述

在企業級應用程序中,存儲部分的技術選型是多樣化的,開發人員可以根據應用的具體情況來選擇合適的存儲技術,比如關系型數據庫或者文檔數據庫、對象數據庫等。為此,Apworks也從框架級別對Repository的定制和二次開發進行支持,目前默認地提供三種Repository的實現:NHibernate Repository、Entity Framework Repository和MongoDB Repository。本文將對MongoDB的Repository設計與實現進行一些簡要的討論。

設計

Apworks為基於第三方框架的組件擴展提供了很好的支持。舉例來說,分離接口Separated Interface)模式使得基於第三方框架的組件擴展和升級能夠獨立於Apworks的核心組件,從而實現在不更改核心組件的情況下,能夠非常方便地引入新的組件或者升級現有組件。就Repository的具體實現而言,Apworks目前已經能夠支持三種不同的技術選型:NHibernate、Entity Framework以及MongoDB。前兩者是基於關系型數據庫和第三方ORM框架的,而后者則是一種較為流行的NoSQL數據存儲方案。

以下是基於MongoDB的Repository實現的整體類結構圖,為了簡化,圖中省略了對類成員的描述:

image

在上圖中,IRepository接口、IRepositoryContext接口以及Repository抽象類都是定義在Apworks的核心部分(即Apworks.dll中),事實上,只要實現了IRepository和IRepositoryContext接口,那么這種Repository的具體實現就可以無縫地整合到Apworks框架中。通過查看Apworks的已有源代碼不難發現,基於NHibernate和Entity Framework的Repository實現,都是遵循這樣的規則。

MongoDBRepositoryContext類在初始化時,構造函數需要接受一個IMongoDBRepositoryContextSettings類型的參數,這個類型包含了MongoDB數據庫服務器和數據庫的設置信息,以及一個通過聚合根的類型來確定MongoDB中Collection名稱的委托屬性。在設計中引入這個接口的目的是:一方面能夠讓開發人員更多地掌握MongoDB的配置方式(比如對服務器和數據庫的配置等),另一方面進一步降低框架與MongoDB之間的銜接關系(比如通過委托屬性來獲得聚合根類型與Collection名稱之間的映射關系)。

另外,基於MongoDB的Repository實現,需要對MongoDB的文檔序列化方式進行一些干預。比如在默認情況下,MongoDB會自動產生ObjectId以作為文檔的主鍵,但Apworks框架中,實體將有自己的主鍵ID,此時就需要將實體的ID用作文檔的主鍵。於是,在MongoDBRepositoryContext中提供了這樣的靜態函數:它允許調用者通過Convention Profile的方式,向MongoDB注冊Convention,以便干預序列化方式。除了需要將實體ID用作文檔主鍵之外,還需要通過以下調用來注冊幾個需要的(不一定是必須的)Convention:

  1. SetIdGeneratorConvention:通過此調用設置ID字段是否需要自動產生,以及ID值的產生方式。此Convention繼承於IIdGeneratorConvention接口
  2. SetSerializationOptionsConvention:通過此調用設置是否使用本地時間來序列化System.DateTime類型。在MongoDB中,DateTime默認是采用UTC形式進行存儲的。此Convention繼承於ISerializationOptionsConvention接口

在實際應用中,開發人員可以通過可選參數來確定是否需要對以上兩種Convention進行注冊,也可以注冊自定義的Convention Profile。由此可見,雖然Apworks對MongoDB的Repository實現進行了一些封裝,但並不會代替開發人員決定些什么,各種面向MongoDB的設置和序列化方式都可以由開發人員完全掌控。

實現

Expression of type <A> cannot be used for return type <B>問題的解決

假設在實體Customer中有一個int類型的Sequence屬性,那么對於下面的查詢,在MongoDB上的執行是正確的:

var query = collection
                      .AsQueryable<Customer>()
                      .Where(p => true)
                      .OrderByDescending(sort => sort.Sequence).ToList();

 

然而,如果使用下面的方式進行查詢,就會拋出這樣的ArgumentException: Expression of type <A> cannot be used for return type <B>:

Expression<Func<Customer, object>> sortPred = sort => sort.Sequence;
var query = collection
                      .AsQueryable<Customer>()
                      .Where(p => true)
                      .OrderByDescending(sortPred).ToList();

 

之所以我們需要保留第二種調用方式,是因為Apworks框架的Repository包含了接受Expression<Func<Customer, dynamic>>類型參數的函數重載,然而這種方式又會產生上面的問題。經過測試,發現NHibernate和Entity Framework的倉儲實現均未出現上述問題。我想這里應該是MongoDB的LINQ Provider在使用ExpressionVisitor對Lambda Expression的處理方式與兩者不同所致。

所以,對於MongoDBRepository的FindAll(Expression<Func<TAggregateRoot, dynamic>> sortPredicate, SortOrder sortOrder)方法,我們就不能簡單地將sortPredicate參數直接傳遞給OrderByDescending擴展方法。

順便說一下,在Apworks的Repository中,用於排序的表達式,我使用了dynamic關鍵字,類似於上面的Expression<Func<Customer, dynamic>>,事實上完全可以使用object。當初原以為可以借用dynamic來解決協變/逆變的問題,但后來發現不行,也就作罷了。

為了能夠解決這個問題,我們需要對Lambda表達式進行修改。就上面的例子而言,雖然FindAll函數接受的是Expression<Func<TAggregateRoot, dynamic>>類型的參數,但OrderByDescending所需要的Lambda Expression應該是Expression<Func<TAggregateRoot, int>>類型,因為Sequence是int類型的。於是,可以使用Expression.Convert方法對sortPredicate中的Property Expression進行類型轉換。比如:

ParameterExpression param = sortPred.Parameters[0];
Expression<Func<Customer, int>> expr = Expression.Lambda<Func<Customer, int>>(
    Expression.Convert(
	    Expression.Property(param, "Sequence"), 
		    typeof(int)), param);

 

此時,再將生成的expr傳遞給OrderByDescending方法,就能夠成功完成排序查詢了。

但事情還沒完,因為我們不能簡單地將expr定義為Expression<Func<Customer, int>>類型,此處是因為Customer的Sequence類型是int型的,但在實際中用於排序的屬性可以是任意類型。理想的做法是將expr定義為Expression<Func<Customer, object>>類型,事實上並沒有辦法將一個具體的基於某個屬性類型的Lambda表達式轉換成Expression<Func<Customer, object>>類型。

經過一段時間的研究,我采用了如下的方法:首先分析給定的sortPredicate所包含的屬性的名稱和類型,然后使用Expression.Lambda靜態方法,將sortPredicate轉換為弱類型的Lambda表達式(即Expression.Lambda調用返回的是一個LambdaExpression,而非Expression<Func<Customer, object>>這樣的類型),然后使用反射來調用Queryable類型上的OrderBy和OrderByDescending靜態方法從而獲得IOrderedQueryable實例。接下來的處理方式就與正常情形一樣了。采用反射來調用Queryable上的靜態方法,是因為OrderBy和OrderByDescending方法無法接受弱類型的Lambda表達式作為傳入參數。

最后,為了不變動已有代碼,我在Apworks.Repositories.MongoDB的Assembly中針對IQueryable添加了兩個擴展方法:OrderBy和OrderByDescending。完整代碼如下:

/// <summary>
/// Represents the helper (method extender) for the sorting lambda expressions.
/// </summary>
internal static class SortExpressionHelper
{
    #region Private Static Methods
    private static IOrderedQueryable<TAggregateRoot> InvokeOrderBy<TAggregateRoot>(IQueryable<TAggregateRoot> query, 
            Expression<Func<TAggregateRoot, dynamic>> sortPredicate, SortOrder sortOrder)
        where TAggregateRoot : class, IAggregateRoot
    {
        var param = sortPredicate.Parameters[0];
        string propertyName = null;
        Type propertyType = null;
        Expression bodyExpression = null;
        if (sortPredicate.Body is UnaryExpression)
        {
            UnaryExpression unaryExpression = sortPredicate.Body as UnaryExpression;
            bodyExpression = unaryExpression.Operand;
        }
        else if (sortPredicate.Body is MemberExpression)
        {
            bodyExpression = sortPredicate.Body;
        }
        else
            throw new ArgumentException(@"The body of the sort predicate expression should be 
                either UnaryExpression or MemberExpression.", "sortPredicate");
        MemberExpression memberExpression = (MemberExpression)bodyExpression;
        propertyName = memberExpression.Member.Name;
        if (memberExpression.Member.MemberType == MemberTypes.Property)
        {
            PropertyInfo propertyInfo = memberExpression.Member as PropertyInfo;
            propertyType = propertyInfo.PropertyType;
        }
        else
            throw new InvalidOperationException(@"Cannot evaluate the type of property since the member expression 
                represented by the sort predicate expression does not contain a PropertyInfo object.");

        Type funcType = typeof(Func<,>).MakeGenericType(typeof(TAggregateRoot), propertyType);
        LambdaExpression convertedExpression = Expression.Lambda(funcType, 
            Expression.Convert(Expression.Property(param, propertyName), propertyType), param);
        
        var sortingMethods = typeof(Queryable).GetMethods(BindingFlags.Public | BindingFlags.Static);
        var sortingMethodName = GetSortingMethodName(sortOrder);
        var sortingMethod = sortingMethods.Where(sm => sm.Name == sortingMethodName &&
            sm.GetParameters() != null &&
            sm.GetParameters().Length == 2).First();
        return (IOrderedQueryable<TAggregateRoot>)sortingMethod
            .MakeGenericMethod(typeof(TAggregateRoot), propertyType)
            .Invoke(null, new object[] { query, convertedExpression });
    }

    private static string GetSortingMethodName(SortOrder sortOrder)
    {
        switch (sortOrder)
        {
            case SortOrder.Ascending:
                return "OrderBy";
            case SortOrder.Descending:
                return "OrderByDescending";
            default:
                throw new ArgumentException("Sort Order must be specified as either Ascending or Descending.", 
				    "sortOrder");
        }
    }
    #endregion

    #region Internal Method Extensions
    /// <summary>
    /// Sorts the elements of a sequence in ascending order according to a lambda expression.
    /// </summary>
    /// <typeparam name="TAggregateRoot">The type of the aggregate root.</typeparam>
    /// <param name="query">A sequence of values to order.</param>
    /// <param name="sortPredicate">The lambda expression which indicates the property for sorting.</param>
    /// <returns>An <see cref="IOrderedQueryable[T]"/> whose elements are sorted according to the lambda expression.</returns>
    internal static IOrderedQueryable<TAggregateRoot> OrderBy<TAggregateRoot>(this IQueryable<TAggregateRoot> query, 
        Expression<Func<TAggregateRoot, dynamic>> sortPredicate)
        where TAggregateRoot : class, IAggregateRoot
    {
        return InvokeOrderBy(query, sortPredicate, SortOrder.Ascending);
    }
    /// <summary>
    /// Sorts the elements of a sequence in descending order according to a lambda expression.
    /// </summary>
    /// <typeparam name="TAggregateRoot">The type of the aggregate root.</typeparam>
    /// <param name="query">A sequence of values to order.</param>
    /// <param name="sortPredicate">The lambda expression which indicates the property for sorting.</param>
    /// <returns>An <see cref="IOrderedQueryable[T]"/> whose elements are sorted according to the lambda expression.</returns>
    internal static IOrderedQueryable<TAggregateRoot> OrderByDescending<TAggregateRoot>(this IQueryable<TAggregateRoot> query, 
        Expression<Func<TAggregateRoot, dynamic>> sortPredicate)
        where TAggregateRoot : class, IAggregateRoot
    {
        return InvokeOrderBy(query, sortPredicate, SortOrder.Descending);
    }
    #endregion
}

 

在實際中應用基於MongoDB的Repository

首先我們需要新建一個實現IMongoDBRepositoryContextSettings接口的類,在類中對服務器、數據庫進行設置,並指定聚合根類型與Collection之間的映射關系。比如:

public class MongoDBRepositoryContextSettings : IMongoDBRepositoryContextSettings
{
    public MongoDBRepositoryContextSettings() { }

    #region IMongoDBRepositoryContextSettings Members

    public MapTypeToCollectionNameDelegate MapTypeToCollectionName
    {
        get
        {
            return null; // Null to use the type name as the collection name.
        }
    }

    public MongoServerSettings ServerSettings
    {
        get
        {
            MongoServerSettings serverSettings = new MongoServerSettings();
            serverSettings.Server = new MongoServerAddress("localhost");
            serverSettings.SafeMode = SafeMode.True;
            return serverSettings;
        }
    }

    public MongoDatabaseSettings GetDatabaseSettings(MongoServer server)
    {
        MongoDatabaseSettings databaseSettings = new MongoDatabaseSettings(server, 
            "MyDatabaseName");
        return databaseSettings;
    }

    #endregion
}

 

之后,修改配置信息,在IoC容器中注冊IMongoDBRepositoryContextSettings、IRepositoryContext以及IRepository類型,以使用MongoDB的實現類,比如對於Unity的IoC容器,可以在web.config/app.config中加入以下的配置信息:

<register type="Apworks.Repositories.MongoDB.IMongoDBRepositoryContextSettings, Apworks.Repositories.MongoDB"
        mapTo="ApworksStarterEF.Domain.Repositories.MongoDBRepositoryContextSettings, ApworksStarterEF.Domain.Repositories">
    <lifetime type="ContainerControlledLifetimeManager"/>
</register>
<register type="Apworks.Repositories.IRepositoryContext, Apworks"
        mapTo="Apworks.Repositories.MongoDB.MongoDBRepositoryContext, Apworks.Repositories.MongoDB">
<lifetime type="Apworks.ObjectContainers.Unity.WcfPerRequestLifetimeManager, Apworks.ObjectContainers.Unity"/>
    <constructor>
      <param name="settings">
        <dependency type="Apworks.Repositories.MongoDB.IMongoDBRepositoryContextSettings, Apworks.Repositories.MongoDB"/>
      </param>
    </constructor>
</register>

 

接下來,別忘了在應用程序的BootStrapper中調用MongoDBRepositoryContext.RegisterConventions靜態方法。

Apworks支持多種配置方式,我們完全可以不使用web.config/app.config對Apworks進行配置,我們可以使用寫代碼的方式,這種方式對於單體測試有很大的幫助。

最后,在使用時,可以直接使用ServiceLocator類來獲取接口的具體實現。因此,我們可以無需修改任何源代碼,就能實現對Repository的替換。

總結

本文簡要地討論了Apworks框架中,基於MongoDB的Repository的設計與實現。接下來我打算對三種不同的Repository做一些性能上的評估。相關的實現代碼可以登錄Apworks的站點http://apworks.codeplex.com,然后查看最新版本的代碼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM