Entity Framework Явная загрузка в пакетном режиме - PullRequest
2 голосов
/ 04 марта 2012

Мне нужно загрузить кучу данных для генерации отчета. В графе участвуют около 8 или 9 различных типов сущностей.

Если я вызову Include для включения всех необходимых мне данных, результирующий запрос будет настолько сложным (левые объединения, объединения, инструкции case в большом количестве), что для его выполнения потребуется около 600 секунд.

Если я позволю ленивой загрузке лениво загружать данные для меня, когда я генерирую отчет, создание отчета занимает около 180 секунд. Лучше, но все же не приемлемо.

Если я сделаю

"загрузить следующий тип объекта на основе идентификаторов связанного объекта типа "

Подход

(вроде LLBLGen, если вы с ним знакомы), я могу получить все необходимые данные примерно за 3 секунды.

Любые предложения о том, как это сделать?

По сути, я хочу сделать что-то вроде этого:

var invoices = objectContext.Invoices.Where(...reportCriteria...).ToArray();

objectContext.LoadProperty(invoices, "InvoiceReceivables");

objectContext.LoadProperty(invoices.SelectMany(i => i.InvoiceReceivables), "Adjustments");

objectContext.LoadProperty(invoices.SelectMany(i => i.InvoiceReceivables.SelectMany(ir => ir.Adjustments)), "AdjustmentComments")

... and so on

Но LoadProperty работает только для отдельных объектов, но не для коллекций.

Есть ли другой способ, кроме выполнения запросов и построения графа объектов самостоятельно?

Ответы [ 2 ]

2 голосов
/ 05 марта 2012

Ответ Althugh EJ вполне верный и, возможно, лучший, более производительный подход, у нас на самом деле нет пропускной способности для реструктуризации отчета прямо сейчас.Я собрал это, и, кажется, добился цели.Может быть, это принесет пользу кому-то другому ... Некоторые краткие вспомогательные методы для краткости опущены.

Использование:

Q.GetQueryableFactory(objectContext).Load(invoices, 
  i => i.InvoiceReceivables.SelectMany(ir => ir.Adjustments.SelectMany(
      a => a.AdjustmentComments)))


public static class Q
{
    /// <summary>
    /// Gets a queryable factory that returns a queryable for a specific entity type.
    /// </summary>
    /// <param name="objectContext">The object context.</param>
    /// <returns></returns>
    public static Func<Type, IQueryable> GetQueryableFactory(object objectContext)
    {
        var queryablePropertiesByType = objectContext.GetType().GetProperties().Where(p => p.GetIndexParameters().Length == 0 && p.PropertyType.IsGenericTypeFor(typeof(IQueryable<>)))
            .ToDictionary(p => p.PropertyType.FindElementType());

        return t =>
                   {
                       PropertyInfo property;
                       if (!queryablePropertiesByType.TryGetValue(t, out property))
                       {
                           property = queryablePropertiesByType.Values.FirstOrDefault(p => p.PropertyType.FindElementType().IsAssignableFrom(t))
                               .EnsureNotDefault("Could not find queryable for entity type {0}.".FormatWith(t.Name));

                           var queryable = property.GetValue(objectContext, null);

                           return (IQueryable)typeof(System.Linq.Queryable).GetMethod("OfType").MakeGenericMethod(t).Invoke(null, new[] { queryable });
                       }

                       return (IQueryable)property.GetValue(objectContext, null);
                   };
    }

    /// <summary>
    /// Loads the target along the specified path, using the provided queryable factory.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="queryableFactory">The queryable factory.</param>
    /// <param name="target">The target.</param>
    /// <param name="path">The path.</param>
    public static void Load<T>(this Func<Type, IQueryable> queryableFactory, T target, Expression<Func<T, object>> path)
    {
        queryableFactory.Load(target, path.AsEnumerable().Reverse().OfType<MemberExpression>().Select(m => m.Member.Name).Join("."));
    }

    /// <summary>
    /// Loads the target along the specified path, using the provided queryable factory.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="queryableFactory">The queryable factory.</param>
    /// <param name="target">The target.</param>
    /// <param name="path">The path.</param>
    public static void Load<T>(this Func<Type, IQueryable> queryableFactory, IEnumerable<T> target, Expression<Func<T, object>> path)
    {
        queryableFactory.Load(target, path.ToMemberAccessStrings().First());
    }

    /// <summary>
    /// Loads the target along the specified path, using the provided queryable factory.
    /// </summary>
    /// <param name="queryableFactory">The queryable factory.</param>
    /// <param name="target">The target.</param>
    /// <param name="path">The path.</param>
    public static void Load(this Func<Type, IQueryable> queryableFactory, object target, string path)
    {
        foreach (var pathPart in path.Split('.'))
        {
            var property = (target.GetType().FindElementType() ?? target.GetType()).GetProperty(pathPart);

            LoadProperty(queryableFactory(property.PropertyType.FindElementType() ?? property.PropertyType), target, pathPart);

            if (target is IEnumerable)
            {
                // select elements along path target.Select(i => i.Part).ToArray()
                target = target.CastTo<IEnumerable>().AsQueryable().Select(pathPart).ToInferredElementTypeArray();

                var propertyElementType = property.PropertyType.FindElementType();
                if (propertyElementType != null)
                {
                    target = target.CastTo<object[]>().SelectMany(i => i.CastTo<IEnumerable>().ToObjectArray()).ToArray(propertyElementType);
                }
            }
            else
            {
                target = property.GetValue(target, null);
            }
        }
    }

    /// <summary>
    /// Loads the property on the target using the queryable source.
    /// </summary>
    /// <param name="source">The source.</param>
    /// <param name="target">The target.</param>
    /// <param name="targetProperty">The target property.</param>
    /// <param name="targetIdProperty">The target id property.</param>
    /// <param name="sourceProperty">The source property.</param>
    public static void LoadProperty(this IQueryable source, object target, string targetProperty, string targetIdProperty = null, string sourceProperty = null)
    {
        var targetType = target.GetType();
        targetType = targetType.FindElementType() ?? (targetType.Assembly.IsDynamic && targetType.BaseType != null ? targetType.BaseType : targetType);

        // find the property on the source so we can do queryable.Where(i => i.???)
        var sourceType = source.ElementType;
        PropertyInfo sourcePropertyInfo;
        if (sourceProperty == null)
        {
            sourcePropertyInfo = sourceType.GetProperty(targetType.Name + "Id") ?? sourceType.GetProperty(targetType.Name + "ID") ?? sourceType.GetProperty("Id") ?? sourceType.GetProperty("ID");
        }
        else
        {
            sourcePropertyInfo = sourceType.GetProperty(sourceProperty);
        }

        if (sourcePropertyInfo == null || sourcePropertyInfo.GetIndexParameters().Length != 0) throw new InvalidOperationException("Could not resolve id property on source {0}.".FormatWith(source.ElementType.Name));


        // find the property on the target so we can find the relevant source objects via queryable.Where(i => i.Property == ???)
        PropertyInfo targetIdPropertyInfo;
        if (targetIdProperty == null)
        {
            targetIdPropertyInfo = targetType.GetProperty(targetProperty + "Id") ?? targetType.GetProperty(targetProperty + "ID") ?? targetType.GetProperty("Id") ?? targetType.GetProperty("Id").EnsureNotDefault("Could not resolve id property to use on {0}.".FormatWith(targetType.Name));
        }
        else
        {
            targetIdPropertyInfo = targetType.GetProperty(targetIdProperty);
        }

        if (targetIdPropertyInfo == null || targetIdPropertyInfo.GetIndexParameters().Length != 0) throw new InvalidOperationException("Could not resolve id property for {0} on target {1}.".FormatWith(targetProperty, targetType.Name));


        var targetPropertyInfo = targetType.GetProperty(targetProperty);
        if (targetPropertyInfo == null || targetPropertyInfo.GetIndexParameters().Length != 0) throw new InvalidOperationException("Could not find property {0} on target type {1}.".FormatWith(targetProperty, target.GetType()));

        // go get the data and set the results.
        if (target is IEnumerable)
        {
            // filter to only non loaded targets
            var nullOrEmptyPredicate = "{0} == null".FormatWith(targetPropertyInfo.Name);
            if (targetPropertyInfo.PropertyType.FindElementType() != null) nullOrEmptyPredicate += " or {0}.Count = 0".FormatWith(targetPropertyInfo.Name);
            target = target.CastTo<IEnumerable>().AsQueryable().Where(nullOrEmptyPredicate).ToInferredElementTypeArray();

            var ids = target.CastTo<IEnumerable>().OfType<object>().Select(i => targetIdPropertyInfo.GetValue(i, null)).WhereNotDefault().Distinct().ToArray();

            if (!ids.Any()) return;

            var predicate = ids.Select((id, index) => "{0} = @{1}".FormatWith(sourcePropertyInfo.Name, index)).Join(" or ");
            // get the results in one shot
            var results = source.Where(predicate, ids.ToArray()).ToInferredElementTypeArray().AsQueryable();

            foreach (var targetItem in target.CastTo<IEnumerable>())
            {
                SetResultsOnTarget(results, targetItem, sourcePropertyInfo, targetIdPropertyInfo, targetPropertyInfo);
            }
        }
        else
        {
            // only fetch if not loaded already
            var value = targetPropertyInfo.GetValue(target, null);
            if (value == null || value.As<IEnumerable>().IfNotNull(e => e.IsNullOrEmpty()))
            {
                SetResultsOnTarget(source, target, sourcePropertyInfo, targetIdPropertyInfo, targetPropertyInfo);
            }
        }

    }

    /// <summary>
    /// Sets the results on an individual target entity.
    /// </summary>
    /// <param name="source">The source.</param>
    /// <param name="target">The target.</param>
    /// <param name="sourcePropertyInfo">The source property info.</param>
    /// <param name="targetIdPropertyInfo">The target id property info.</param>
    /// <param name="targetPropertyInfo">The target property info.</param>
    private static void SetResultsOnTarget(IQueryable source, object target, PropertyInfo sourcePropertyInfo, PropertyInfo targetIdPropertyInfo, PropertyInfo targetPropertyInfo)
    {
        var id = targetIdPropertyInfo.GetValue(target, null);

        var results = source.Where("{0} = @0".FormatWith(sourcePropertyInfo.Name), id).As<IEnumerable>().OfType<object>().ToArray();

        var targetPropertyElementType = targetPropertyInfo.PropertyType.FindElementType();
        if (targetPropertyElementType != null)
        {
            // add all results
            object collection = targetPropertyInfo.GetValue(target, null);

            if (collection == null)
            {
                // instantiate new collection, otherwise use existing
                collection = Activator.CreateInstance(typeof(List<>).MakeGenericType(targetPropertyElementType));
                targetPropertyInfo.SetValue(target, collection, null);
            }

            var addMethod = collection.GetType().GetMethods().FirstOrDefault(m => m.Name == "Add" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsAssignableFrom(targetPropertyElementType)).EnsureNotDefault("Could not find add method for collection type.");

            foreach (var result in results)
            {
                if (!Enumerable.Contains((dynamic)collection, result)) addMethod.Invoke(collection, new[] { result });
            }
        }
        else
        {
            targetPropertyInfo.SetValue(target, results.FirstOrDefault(), null);
        }
    }
}
1 голос
/ 04 марта 2012

Я использую прямой EF4.0 для всех моих данных доступа к аспекту пользовательского интерфейса моих приложений, но когда дело доходит до отчетов, я почти всегда отказываюсь от «пути EF» и пишу отчеты против хранимой процедуры и / илиТаким образом, вся сложная и трудоемкая логика / свертки может быть выполнена своевременно прямо на сервере БД.Таким образом, производительность всегда великолепна.

Что стоит учесть, если вы не можете заставить EF делать это достаточно быстро, чтобы удовлетворить ваши потребности.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...