您的位置:首页 > 其它

EF学习笔记28:如何实现自己的预先加载(Eager Loading)策略

2010-08-26 16:25 609 查看
背景: 在过去的两年中,很对开发者都抱怨EF中预先加载的执行方式。

下面是你经常会使用的预先加载的方式:

var results = from b in ctx.Blogs.Include(“°Posts”±)
where b.Owner == “°Alex”±
select b;



上面代码的意思很明确,要求EF预先加载所有符合要求的Blog的相关的Posts,代码会很好的执行。

但是问题是这里使用了"Posts”这样的字符串的方式,这对于已经习惯于在常规的Linq语句或者Linq to SQL语句中使用强类型的我们来说,用string的这样类型不安全的方式显然不是我们所希望的,我们希望的形式或许是这样的:

var results = from b in ctx.Blogs.Include(b => b.Posts)
where b.Owner == “°Alex”±
select b;
上面这样的方式是类型安全的,很多人也尝试着这样做。


下面的方式或许更好:

var strategy = new IncludeStrategy<Blog>();
strategy.Include(b => b.Owner);

var results = from b in strategy.ApplyTo(ctx.Blogs)
where b.Owner == “°Alex”±
select b;



上面这样的方式可以让你在不同的数据检索之间重用strategies。

设计目标:

根据上面的想法,我打算扩展我的思路来支持这里的strategies。

 

下面是我想做的针对Blog的Stretegy方式的类的实力:

var strategy = Strategy.NewStrategy<Blog>();
strategy.Include(b => b.Owner)
.Include(p => p.Comments); //sub includes
strategy.Include(b => b.Posts);    //multiple includes

上面的类将支持子类继承:
public class BlogFetchStrategy: IncludeStrategy<Blog>
{
public BlogFetchStrategy()
{
this.Include(b => b.Owner);
this.Include(b => b.Posts);
}
}



有了上面的类,你可以像下面这样来使用:

var results = from b in new BlogFetchStrategy().ApplyTo(ctx.Blogs)
where b.Owner == “°Alex”±
select b;



实现:

下面我们来实现上面的这个stretegy类:

1) 创建 IncludeStrategy<T> 类:

public class IncludeStrategy<TEntity>
where TEntity : class, IEntityWithRelationships
{
private List<string> _includes = new List<string>();

public SubInclude<TNavProp> Include<TNavProp>(
Expression<Func<TEntity, TNavProp>> expr
) where TNavProp : class, IEntityWithRelationships
{
return new SubInclude<TNavProp>(
_includes.Add,
new IncludeExpressionVisitor(expr).NavigationProperty
);
}

public SubInclude<TNavProp> Include<TNavProp>(
Expression<Func<TEntity, EntityCollection<TNavProp>>> expr
) where TNavProp : class, IEntityWithRelationships
{
return new SubInclude<TNavProp>(
_includes.Add,
new IncludeExpressionVisitor(expr).NavigationProperty
);
}

public ObjectQuery<TEntity> ApplyTo(ObjectQuery<TEntity> query)
{
var localQuery = query;
foreach (var include in _includes)
{
localQuery = localQuery.Include(include);
}
return localQuery;
}
}



从上面的代码中我们注意到,代码使用了一个string数组来存储我们要添加的Includes,同时还有ApplyTo(…)方法允许调用者

可以将所有符合T类型的ObjectQuery<T>, 绑定到Includes中来。

淡然代码主要的功能实在两个Include(..) 方法中,之所以要有两个重载方法,其中第一个是为了加载References引用,另一个是为了加载数组。这样的实现是针对.NET 3.5 SP1的,所以我可以依赖于那些实现了IEntityWithRelationships接口,并具有relationships的类(可以对包含Include的类进行检测)。有趣的是真毒载入数组的Include方法,每一个Expression语句都被设置成isExpression<Func<TEntity, EntityCollection<TNavProp>>> ,而在创建Sub-includes的时候返回的类型却是TNavProp,这样的做法让我们避免在使用的时候使用如下的代码:

Include(b => b.Posts.SelectMany(p => p.Author));

同时也可以避免下面的写法:

Include(b => b.Posts.And().Author);

取而代之的是下面的更简洁和合理的写法:

Include(b => b.Posts).Include(p => p.Author);

上面的实现方式更简单,这也是整个设计中的核心部分。

2) 类IncludeExpressionVisitor 派生自ExpressionVisitor这个实例,你可以在这里找到它,这个实例其实非常简单,或许用在这里有些有过度设计的感觉,但我喜欢在这里采用正确的是寂寞死来实现这个功能。

public class IncludeExpressionVisitor : ExpressionVisitor
{
private string _navigationProperty = null;

public IncludeExpressionVisitor(Expression expr)
{
base.Visit(expr);
}
public string NavigationProperty
{
get { return _navigationProperty; }
}

protected override Expression VisitMemberAccess(
MemberExpression m
)
{
PropertyInfo pinfo = m.Member as PropertyInfo;

if (pinfo == null)
throw new Exception(
"You can only include Properties");
if (m.Expression.NodeType != ExpressionType.Parameter)
throw new Exception(
"You can only include Properties of the Expression Parameter");
_navigationProperty = pinfo.Name;

return m;
}

protected override Expression Visit(Expression exp)
{
if (exp == null)
return exp;
switch (exp.NodeType)
{
case ExpressionType.MemberAccess:
return this.VisitMemberAccess(
(MemberExpression)exp
);
case ExpressionType.Lambda:
return this.VisitLambda((LambdaExpression)exp);
default:
throw new InvalidOperationException(
"Unsupported Expression");
}
}
}



从上面的代码中你可以看到,vistor仅仅是起了一个约束的作用,它仅仅能够识别LambdaExpressions和MemberExpressions。

当使用MemberExpression的时候,visitor将确认进入的成员是一个属性(Property),并把这个成员直接绑定到其参数中(Parameter)中(比如p.Property可以,但是p.Property.SubProperty不可以)如果出现这种情况,visitor将会记录下NavigationProperty的名字。

3)当我们知道NavigationProperty的名字,IncludeStrategy.Include 方法会创建一个SubInclude<T> 对象,它将用来注册NavigationProperty,并为串联多个sub-includes提供相关的机制。

类SubInclude<T>的代码如下:

public class SubInclude<TNavProp>
where TNavProp : class, IEntityWithRelationships
{

private Action<string> _callBack;
private string[] _paths;
internal SubInclude(Action<string> callBack, params string[] path)
{
_callBack = callBack;
_paths = path;
_callBack(string.Join(".", _paths));
}

public SubInclude<TNextNavProp> Include<TNextNavProp>(
Expression<Func<TNavProp, TNextNavProp>> expr
) where TNextNavProp : class, IEntityWithRelationships
{
string[] allpaths = _paths.Append(
new IncludeExpressionVisitor(expr).NavigationProperty
);
return new SubInclude<TNextNavProp>(_callBack, allpaths);
}

public SubInclude<TNextNavProp> Include<TNextNavProp>(
Expression<Func<TNavProp, EntityCollection<TNextNavProp>>> expr
) where TNextNavProp : class, IEntityWithRelationships
{
string[] allpaths = _paths.Append(
new IncludeExpressionVisitor(expr).NavigationProperty
);
return new SubInclude<TNextNavProp>(_callBack, allpaths);
}
}



4) 现在还遗留的一点东西是一个用于附加其他元素到array数组中的扩展方法,代码如下:

public static T[] Append<T>(this T[] initial, T additional)
{
List<T> list = new List<T>(initial);
list.Add(additional);
return list.ToArray();
}



有了上面的代码,你就可以以你自己的方式非常容易的实现与预先加载,你所需要做的就是继承类IncludeStrategy<T>.

但是要注意的是,这个方案只是我的个人方案,并不是微软的官方解决方案,它没有经过严格测试。你可愿意在这里下载到完整代码。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐