海豚宝宝的代码生活

专注于.Net系统开发

导航

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

背景:

在过去的两年中,很对开发者都抱怨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>.

但是要注意的是,这个方案只是我的个人方案,并不是微软的官方解决方案,它没有经过严格测试。你可愿意在这里下载到完整代码。

posted on 2010-08-26 16:25  Bruse  阅读(465)  评论(0)    收藏  举报