Nhibernate。模拟会话上的Fetch调用失败

时间:2015-10-22 16:43:41

标签: c# unit-testing nhibernate linq-to-nhibernate

我喜欢NHibernate(以及NHibernate.Linq)。我没有过早地优化,但有时我会遇到一个非常令人讨厌的N + 1问题。 N + 1的推荐修复方法是使用NH的Fetch扩展方法。

当我创建ISession的模拟时出现问题。我会创建一个List<User>并设置我的模拟,以便在有人调用_session.Query<User>()时返回列表。当我向查询添加Fetch次调用时(即_session.Query<User>().Fetch(u => u.Address)),我收到以下错误消息:

There is no method 'Fetch' on type 'NHibernate.Linq.EagerFetchingExtensionMethods' 
that matches the specified arguments

NHibernate的fetch接受一个普通的IQueryable<T>但尝试将其作为特定的NH实现进行转换,如果不能,则会失败。

如果在非NH实现(即列表)上调用它,我真的希望Fetch不出错,只是被忽略,所以我仍然可以在我的单元测试中使用它。救命啊!

3 个答案:

答案 0 :(得分:4)

嗯,我试图自己实现这个,但感谢上帝,我找到了一个已经做过腿部工作的人。

http://mycodinglife.blog.com/2013/06/10/fetch-good-boy-now-play-nice-with-my-unit-tests/#

您唯一需要做的就是致电EagerlyFetch,而不仅仅是Fetch

我已经复制了下面的相关代码,因为他的博客已经有相当数量的http 500错误和css问题。我不认为它正在被维护。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using NHibernate.Linq;
using Remotion.Linq;

namespace LittleFish.Persistence.Extensions
{
    /// <summary>
    /// Provides extension method wrappers for NHibernate methods 
    /// to allow consuming source code to avoid "using" NHibernate.
    /// </summary>
    public static class NHibernateExtensions
    {
        /// <summary>
        /// Eager-loads a projection of the specified queryable, 
        /// referencing a mapped child object.
        /// </summary>
        public static IFetchRequest<T, TRel> EagerlyFetch<T, TRel>(
            this IQueryable<T> queryable,
            Expression<Func<T, TRel>> expression)
        {
            if (queryable is QueryableBase<T>)
                return FetchHelper.Create(queryable.Fetch(expression));
            else
                return FetchHelper.CreateNonNH<T, TRel>(queryable);
        } 

        /// <summary>
        /// Eager-loads a second-level projection of the specified queryable, 
        /// referencing a mapped child of the first eager-loaded child.
        /// </summary>
        public static IFetchRequest<T, TRel2> ThenEagerlyFetch<T, TRel, TRel2>(
            this IFetchRequest<T, TRel> queryable,
            Expression<Func<TRel, TRel2>> expression)
        {
            if (queryable is QueryableFetchHelper<T, TRel>)
                return FetchHelper.CreateNonNH<T, TRel2>(queryable);
            else
                return FetchHelper.Create(queryable.ThenFetch(expression));
        } 

        /// <summary>
        /// Eager-loads a projection of the specified queryable, 
        /// referencing a mapped child object.
        /// </summary>
        public static IFetchRequest<T, TRel> EagerlyFetchMany<T, TRel>(
            this IQueryable<T> queryable,
            Expression<Func<T, IEnumerable<TRel>>> expression)
        {
            if(queryable is QueryableBase<T>)
                return FetchHelper.Create(queryable.FetchMany(expression));
            else
                return FetchHelper.CreateNonNH<T, TRel>(queryable);
        } 

        /// <summary>
        /// Eager-loads a second-level projection of the specified queryable, 
        /// referencing a mapped child of the first eager-loaded child.
        /// </summary>
        public static IFetchRequest<T, TRel2> ThenEagerlyFetchMany
            <T, TRel, TRel2>(
            this IFetchRequest<T, TRel> queryable,
            Expression<Func<TRel, IEnumerable<TRel2>>> expression)
        {
            if (queryable is QueryableFetchHelper<T, TRel>)
                return FetchHelper.CreateNonNH<T, TRel2>(queryable);
            else
                return FetchHelper.Create(queryable.ThenFetchMany(expression));
        }
    } 

    /// <summary>
    /// Provides a wrapper for NHibernate's FetchRequest interface, 
    /// so libraries that run eager-loaded queries don't have to reference 
    /// NHibernate assemblies.
    /// </summary>
    public interface IFetchRequest<TQuery, TFetch> :
        INhFetchRequest<TQuery, TFetch>
    {
    } 

    internal class NhFetchHelper<TQuery, TFetch> : IFetchRequest<TQuery, TFetch>
    {
        private readonly INhFetchRequest<TQuery, TFetch> realFetchRequest;

        //this is the real deal for NHibernate queries
        internal NhFetchHelper(INhFetchRequest<TQuery, TFetch> realFetchRequest)
        {
            this.realFetchRequest = realFetchRequest;
        } 

        public IEnumerator<TQuery> GetEnumerator()
        {
            return (realFetchRequest).GetEnumerator();
        } 

        IEnumerator IEnumerable.GetEnumerator()
        {
            return (realFetchRequest).GetEnumerator();
        } 

        public Expression Expression
        {
            get { return (realFetchRequest).Expression; }
        } 

        public Type ElementType
        {
            get { return (realFetchRequest).ElementType; }
        } 

        public IQueryProvider Provider
        {
            get { return (realFetchRequest).Provider; }
        }
    } 

    internal class QueryableFetchHelper<TQuery, TFetch> :
        IFetchRequest<TQuery, TFetch>
    {
        private readonly IQueryable<TQuery> queryable;

        //for use against non-NH datastores
        internal QueryableFetchHelper(IQueryable<TQuery> queryable)
        {
            this.queryable = queryable;
        } 


        public IEnumerator<TQuery> GetEnumerator()
        {
            return (queryable).GetEnumerator();
        } 

        IEnumerator IEnumerable.GetEnumerator()
        {
            return (queryable).GetEnumerator();
        } 


        public Expression Expression
        {
            get { return (queryable).Expression; }
        } 

        public Type ElementType
        {
            get { return (queryable).ElementType; }
        } 

        public IQueryProvider Provider
        {
            get { return (queryable).Provider; }
        }
    } 

    /// <summary>
    /// The static "front door" to FetchHelper, with generic factories allowing 
    /// generic type inference.
    /// </summary>
    internal static class FetchHelper
    {
        public static NhFetchHelper<TQuery, TFetch> Create<TQuery, TFetch>(
            INhFetchRequest<TQuery, TFetch> nhFetch)
        {
            return new NhFetchHelper<TQuery, TFetch>(nhFetch);
        } 

        public static NhFetchHelper<TQuery, TFetch> Create<TQuery, TFetch>(
            IFetchRequest<TQuery, TFetch> nhFetch)
        {
            return new NhFetchHelper<TQuery, TFetch>(nhFetch);
        } 

        public static IFetchRequest<TQuery, TRel> CreateNonNH<TQuery, TRel>(
            IQueryable<TQuery> queryable)
        {
            return new QueryableFetchHelper<TQuery, TRel>(queryable);
        }
    }
}

答案 1 :(得分:1)

Fetch是一种来自NHibernate.Linq.EagerFetchingExtensionMethods的扩展方法,这就是为什么你不能嘲笑它。如果您接受原始生产代码的修改,则可以使用包装器。 Wrapper是你将进一步模拟的代码!

不是以流畅的方式调用Fetch(query.Fetch(...)),而是可以调用包装器并将查询作为参考注入:

NHibernateExtensionsWrapper.Fetch(query, x => x.ChildTable).ToList();

如何实现这个包装器?

public class NHibernateExtensionsWrapper : INHibernateExtensionsWrapper
{
    public INhFetchRequest<TOriginating, TRelated> Fetch<TOriginating, TRelated>(IQueryable<TOriginating> query,
        Expression<Func<TOriginating, TRelated>> relatedObjectSelector)
    {
        return query.Fetch(relatedObjectSelector);
    }
}

如何实现包装器模拟?

public class NHibernateExtensionsWrapperMock : INHibernateExtensionsWrapper
{
    public INhFetchRequest<TOriginating, TRelated> Fetch<TOriginating, TRelated>(IQueryable<TOriginating> query, Expression<Func<TOriginating, TRelated>> relatedObjectSelector)
    {
        return (INhFetchRequest<TOriginating, TRelated>) new NhFetchRequest<TOriginating, TRelated>(query);
    }

    private class NhFetchRequest<TOriginating, TRelated> : INhFetchRequest<TOriginating, TRelated>
    {
        private readonly IQueryable<TOriginating> _query;

        public NhFetchRequest(IQueryable<TOriginating> query)
        {
            _query = query;
        }

        public IEnumerator<TOriginating> GetEnumerator()
        {
            return _query.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public Expression Expression => _query.Expression;

        public Type ElementType => _query.ElementType;

        public IQueryProvider Provider => _query.Provider;
    }
}

答案 2 :(得分:0)

我想出了一个新的解决方法,因为我不想为我们在生产代码中使用的每个提取调用添加类型检查,或者不想围绕所有现有的.Fetch调用进行更改。 / p>

.Fetch失败的原因与.NET内部重写表达式树的原因有关。我无法弄清楚到底是怎么回事,但是它似乎取决于表达式.List<T>的输入,它实际上是使用typeof(IQueryable<T>).IsAssignableFrom(typeof(List<T>))来检查输入匹配项,该返回的结果为false。 >

我已经在使用包装器,而不是返回普通的List<T>(取自this answer)来解决.ToFuture()在单元测试中不起作用的问题。

从链接的答案代码中,我将以下内容添加到ExpressionTreeModifier

protected override Expression VisitMethodCall(MethodCallExpression node)
{
    //Don't overwrite if fetch wasn't the method being called
    if (!node.Method.Name.Equals(nameof(EagerFetchingExtensionMethods.Fetch))
        && !node.Method.Name.Equals(nameof(EagerFetchingExtensionMethods.FetchMany))
        && !node.Method.Name.Equals(nameof(EagerFetchingExtensionMethods.ThenFetch))
        && !node.Method.Name.Equals(nameof(EagerFetchingExtensionMethods.ThenFetchMany)))
    {
        return base.VisitMethodCall(node);
    }

    //Get the first argument to the Fetch call. This would be our IQueryable or an Expression which returns the IQueryable.
    var fetchInput = node.Arguments[0];

    Expression returnExpression;

    switch (fetchInput.NodeType)
    {
        case ExpressionType.Constant:
            //If the input was a constant we need to run it through VisitConstant to get the underlying queryable from NHibernateQueryableProxy if applicable
            returnExpression = VisitConstant((ConstantExpression)fetchInput);
            break;
        default:
            //For everything else just return the input to fetch
            //This is covers cases if we do something like .Where(x).Fetch(x), here fetchInput would be another method call
            returnExpression = fetchInput;
            break;
    }

    return returnExpression;
}

这是在表达式执行之前重写表达式,并完全删除了Fetch调用,因此我们永远不会以Linq内部结构来尝试调用它。