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>.

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

你可能感兴趣的:(学习笔记)