您的位置:首页 > 数据库 > Mongodb

浅谈Apworks对MongoDB的支持:设计与实现

2012-07-23 22:41 363 查看

概述

在企业级应用程序中,存储部分的技术选型是多样化的,开发人员可以根据应用的具体情况来选择合适的存储技术,比如关系型数据库或者文档数据库、对象数据库等。为此,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实现的整体类结构图,为了简化,图中省略了对类成员的描述:





在上图中,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:

SetIdGeneratorConvention:通过此调用设置ID字段是否需要自动产生,以及ID值的产生方式。此Convention继承于IIdGeneratorConvention接口 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,然后查看最新版本的代码。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: