zoukankan      html  css  js  c++  java
  • 浅谈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,然后查看最新版本的代码。

  • 相关阅读:
    nginx优化之配置文件优化一常用参数
    Centos7+LVS-DR+Apache负载均衡web实验
    LVS负载均衡原理
    Centos7+LVS-NAT+apache实验
    CentOS7.6 使用ceph-deploy部署mimic 13.2.8集群(一)
    linux关闭ACPI电源管理模块
    Jenkins + pipeline + Git + PHP (九)
    Jenkins-Master-slave架构(八)
    Jenkins参数化构建(七)
    Jenkins连接Git仓库时候报错Permission denied, please try again.
  • 原文地址:https://www.cnblogs.com/daxnet/p/2605695.html
Copyright © 2011-2022 走看看