IEnumerable<T> и IQueryable<T>, в чем разница?

    Уважаемые Хабровчане, решил поделиться одним не очевидным моментом использования LinqToSql, с которым столкнулся некоторое время назад. А именно, про особенности использования каскадных Linq запросов:

    Работая над очередным ASP.NET MVC проектом, и проектируя уровень доступа к БД мне потребовалось проверить качество скриптов, генерируемых Framework-ом L2C.

    Что имеем (упрощенный вариант модели):


    class User {
      public long Id { get; set; }
      public string Name { get; set; }
      public IEnumerable<Parameter> Parameters { get; set; }
    }

    class Parameter {
      public long UserId { get; set; }
      public string Name { get; set; }
      public string Value { get; set; }
    }

    Для получения данных написан класс UserRepository:

    class UserRepository: MyProjectDataContext {
      public IEnumerable GetUsers() {
        return this.Users.ToModelUsers();
      }
    }

    static class RepositoryHelper {
      public static IEnumerable<Model.User> ToModelUsers(this IEnumerable<DataAccess.User> users) {
        return users.Select(u => new Model.User { Id = u.Id, Name = u.Name, Parameters = u.Parameters.ToModelParameters() });
      }

      public static IEnumerable<Model.Parameter> ToModelParameters(this IEnumerable<DataAccess.Parameter> parameters) {
        return parameters.Select(u => new Model.Parameter {… });
      }
    }

    пишем
    var users = userRepository.GetUsers().ToList();

    смотрим профайлер, и с удивлением обнаруживаем что для загрузки 10 пользователей и их параметров было выполнено целых 11 запросов.

    Пытаюсь излечить данную проблему, меняю код репозитория у на следующий:

    public IEnumerable<User> GetUsers() {
      return this.Users.ToModelUsers().Select(u => new Model.User {
        Id = u.Id, Name = u.Name, Parameters = u.Parameters.Select(p => new Model.Parameter {… })
      });
    }

    Картина меняется кардинально. Всего 1 запрос, в котором грузятся как user-ы так и Parameter-ы

    Где собака зарыта?


    Все очень просто, все дело в том в первоначальном варианте после вызова метода GetUsers мы получали запрос содержащий 2 разных выражения: Linq2Sql и Linq2Object, а во втором только Linq2Sql.

    А теперь выводы


    Как оказалось, Linq Extension-ы ведут себя совершенно по разному в случае использования переменной типа IEnumerable и IQueryable, и это не случайно. Дело в том, что метод user.Select(...) есть у обоих классов System.Linq.Enumerable и System.Linq.Queryable соответственно, но реализация конечно же отличается (в чем легко убедиться с помощью Reflector-а).

    Что делать?


    Есть 2 пути, либо указывать тип IQueryable<T> для возвращаемого значения

    static class RepositoryHelper {
      public static IQueryable<Model.User> ToModelUsers(this IQueryable<DataAccess.User> users) {
        return users.Select(u => new Model.User { Id = u.Id, Name = u.Name, Parameters = u.Parameters.ToModelParameters() });
      }
    }

    либо явно приводить IEnumerable<T> к IQueryable<T> вызовом метода .AsQueryable<T>()

    А самое важное — оставаться внимательным и не забывать, что скрывается за ситаксическим сахаром фич .Net 3.5 / 4.0
    Share post

    Similar posts

    Comments 35

      +6
      Привет!
      Всё верно. Добавлю: я в своих проектах стараюсь абсолютно все linq-запросы оформлять в виде скомпилированных expressions. Делается это при помощи хэлпера CompiledQuery.Compile(). Это позволяет избежать путаницы (зачастую из-за недостатка должного внимания), а так же значительно увеличивает производительность выполнения запроса.
        0
        Да ты прав, тоже очень полезная возможность для оптимизации.
          +1
          Про это стоит написать отдельную статью на Хабре про оптимизацию LINQ-запросов. Напишете?
            0
            Да, хорошо. Однако я использую не LinqToSql, а EntityFramework в .net 4.0. Привет!
              0
              Тем более пиши, потому как Entity теперь в тренде вместо Linq2Sql. Я бы вообще предложил начинать потихоньку отказываться от Linq2Sql — развивают-то Entity.
                0
                Да, новый EF из .NET 4.0. сильно отличается от старого, очень сильно, мне нравится, они вставили и POCO объекты как в NHibernate, и можно в рантайме схему генерить, и LinqToEntities отшлифовали, может быть через пару месяцев напишу, а то сейчас горячка с проектом одним. Привет!
                  0
                  Привет!
              0
              http://habrahabr.ru/blogs/net/47336/
              Возможно будет полезным — когда-то писал
            +1
            Такое ощущение, что вы не шарите, почему в Linq2Sql используется Expression<Func<..>>, а не Func<..>
              –5
              Я очень рад что вы, уважаемый в этом шарите. Но вы в этом не одиноки. Есть и другие избранные, которые понимают магию Expression<Func<..>> и умеют ее использовать :)
                +8
                Ммм. Отлично. Было бы еще лчше, если бы другие избранные, про которых вы говорите, вам с самого начала пояснили, почему в вашем случае linq-запрос не достраивался, а распадался на sql-objects-sql, потому что такую «засаду» вы выяснили чисто случайно :) Надеюсь, в вашем проекте это было единственное место.

                P.S. У нас бы за такое в процессе codereview погнали бы ссаными тряпками.
                  +2
                  Ну так ждем от вас доступного объяснения. Давайте, доступное объяснение в студию!
                    +5
                    Как тока мне вернут мою карму на место, так сразу… Иш, потянулись уже ручонки у кого-то.
                    Шутка.

                    На самом деле, вы уже написали половину ответа.
                    Тот Select, который вы делаете по IEnumerable[T], требует наличия сущностей в памяти. Поэтому IQueryable приводится к IEnumerable (своего рода слайсинг) единственным доступным ему способом — он делает то, что в ORM называется материализацией сущностей. После чего навигационный доступ в памяти по одному отправляет запросы в БД за параметрами.
                    Во втором случае IQueryable[T] комбинирует запрос, достраивая join. Потому что он знает целевой язык (sql), и потому что результаты первой части запроса еще не материализованы, а представлены в виде Expression[Func[T]].
                    Я кончил (доклад) и закурил
                      +3
                      Ваш код эквивалентен следующему:
                      foreach (DataAccess.User u in users) (1)
                      {
                      	Model.User user = new Model.User();
                      	user.id = u.id;
                      	user....
                      	user.Parameters = parameters.Select(u => new Model.Parameter {… }); (2)
                      	yeld return user;
                      }
                      

                      Происходит первый вызов в строке (1) и потом еще 10 раз в строке (2)
                +7
                RepositoryHelper отвратительно.
                  0
                  Хех, согласен на миллион процентов. Дело в том, что LINQ 2 SQL — это и не реализация паттерна ActiveRecord, где можно было бы со спокойной душой забыть про слово репозиторий и позволить сущностям самим знать все о доступе к данным (без DataContext). И, тем более, L2S — никаким боком и рядом не стоит с парадигмами DDD (Domain-Driven-Design), чтобы пытаться им следовать.

                  Вот и получаются всякие RepositoryHelper'ы и HelperRepository'и :)
                    +3
                    Мне кажется, что речь была не про «забыть про слово репозиторий», про концовку в виде «Helper».

                    Просто ходят слухи, то полезно называть класс так, чтоб он описывал то, что делает. К примеру ModelUserProvider/Convertor.

                    Часто советуют избегать приставок из серии Heler/Utils аргументируя тем, что это смахивает «процедурным программированием» и не отображает своей сути.

                    Не знаю, правда это или нет, но вот есть такое мнение («и не только мое» (с))
                      0
                      Нет, обычно в .NET используется название Helper, чтобы заключить в этот класс дополнительный функционал, построенный поверх уже существующего в основном классе, чтобы не перегружать последний. Extension-методы в .NET — по сути, созданы для той же цели.
                      Также бывает, что Helper-классы содержат некоторую логику, не имеющую прямого отношения к тому, что делает сам объект (обычно это «аспекты»/AOP). Впрочем, в этом случае, логику содержат не сами эти Helper-классы, а другие классы, используемые внутри хелперов.

                      Приведенный пример в статье, с RepositoryHelper, дейстительно, не отражает сути того, что он делает, и более того — содержит логику, которая должна, по идее быть либо в репозитории, либо в слое сервисов (Application Services).
                        0
                        Да это все ясно, но вот как мне кажется, суть комментария hazzik в том, что возможно стоит называть классы именно так, чтоб названия описывали то, что они делают. От «расширителей», «помощников», «фасадов» и т.д. все равно никуда не денемся.

                        Просто когда немного поработаешь в крупных проектах, то столько насмотришься на всякие Utils/Helper-а что невольно начинает типать когда смотришь на них. При чем могут быть десятки классов с одним названием (типа PageHelper), содержащий совершенно разных «вспомогательный» функционал.

                        В прочем, nevermind, думаю поднятая тема никак не касается именования классов.
                  +4
                  Это уже работая над «очередным» проектом, вы только столкнулись с сутью проблемы SELECT N+1? :)
                    +8
                    Очевидно, что автор не в теме.
                      +1
                      dmitry_h, честно, не создается впечатление о том, что вы разбираетесь в этом. Не обижайтесь, здесь мы для того, чтобы помогать друг другу. Советую вам ознакомиться с книгой:

                      LINQ. Карманный справочник. Албахари Б. Албахари Д.

                      Бумажный вариант:
                      shop.top-kniga.ru/books/item/in/395268/

                      Электронный:
                      www.twirpx.com/file/156984/
                        –4
                        Не люблю тех. литературу на русском(
                        Да и есть уже куда более новые книги по Linq
                        +5
                        Как вам верно написали выше, вы просто не понимаете, как внутри себя работает Linq2Sql. А никакого (ровным счетом никакого) отношения к интерфейсам IEnumerable/IQueryable описанная вами проблема не имеет. Если вы решите просто привести одно к другому, запросов в базу это не уменьшит.

                        Потому что, вы не поверите, стоит при создании датаконтекста указать правильные LoadOptions, как в первом же вашем примере будет один запрос, а не N+1 (вне зависимости от всех ваших измышлений с типами).
                          +2
                          На всякий случай проиллюстрирую мысль «Если вы решите просто привести одно к другому, запросов в базу это не уменьшит.»

                          Что делает вызов Enumerable.Select(source, selector)? Если выкинуть обработку параметров, то он сведется к return WhereSelectEnumerableIterator(source, null, selector);

                            +1
                            (… тьфу ты, продолжаем)

                            Что делает вызов Queryable.Select(source, selector)? Создает Expression, который описывает… вызов метода Select на source (с параметром selector). Кем будет выполнен этот expression? QueryProvider-ом, заданный в source (а source, напомню, пришел к нам из AsQueryable). А вот теперь посмотрим на AsQueryable: EnumerableQuery.Create(..., source), что, в свою очередь, дает нам создание EnumerableQuery(Of T), которая является провайдером сама для себя. Что делает ее Execute? В итоге, после множества перенаправлений, выясняется, что он всего лишь компилирует expression и вызывает его.

                            Вот и все.

                            Подводя итог: AsQueryable — это всего лишь враппер, работа которого все равно сведется к вызову оригинальных методов на Enumerable.

                              0
                              Для тех кто в танке:

                              Так выглядит метод статического класса System.Linq.Queryable

                              public static IQueryable Select<TSource, TResult>(this IQueryable source, Expression<Func<TSource, TResult>> selector)
                              {
                              if (source == null)
                              {
                              throw Error.ArgumentNull(«source»);
                              }
                              if (selector == null)
                              {
                              throw Error.ArgumentNull(«selector»);
                              }
                              return source.Provider.CreateQuery(Expression.Call(null, ((MethodInfo) MethodBase.GetCurrentMethod()).MakeGenericMethod(new Type[] { typeof(TSource), typeof(TResult) }), new Expression[] { source.Expression, Expression.Quote(selector) }));
                              }

                              а так System.Linq.Enumerable:

                              public static IEnumerable Select<TSource, TResult>(this IEnumerable source, Func<TSource, int, TResult> selector)
                              {
                              if (source == null)
                              {
                              throw Error.ArgumentNull(«source»);
                              }
                              if (selector == null)
                              {
                              throw Error.ArgumentNull(«selector»);
                              }
                              return SelectIterator<TSource, TResult>(source, selector);
                              }

                              О чем говорит этот код?

                              От том что логика формирования выражения совершенно разная, в первом случае мы вызываем 'компилятор' выражения, который в зависимости от типа провайдера соответствующим образом преобразует Expression. Во втором случае просто создается итератор, на каждой итерации которого просто вызывается selector.

                                +1
                                Ох. Вы мой коммент-то прочитали? source.Provider для Queryable берется из переданного объекта. Ваш передаваемый объект — это (некий enumerable).AsQueryable. Внутри AsQueryable происходит обертка enumerable в QueryProvider. Это и есть ваш «компилятор выражения». Посмотрите в его код (упрощенно):

                                EnumerableQuery.CreateQuery(Expression expression)
                                {
                                return new EnumerableQuery(expression);
                                }

                                EnumerableQuery.cctor(Expression expression)
                                {
                                this.expression = expression;
                                }

                                EnumerableQuery.Execute(Expression expression)
                                {
                                return new EnumerableExecutor(expression).Execute();
                                }

                                EnumerableExecutor.Execute(Of T)()
                                {
                                if (this.func == null)
                                {
                                EnumerableRewriter rewriter = new EnumerableRewriter();
                                this.func = Expression.Lambda(Of Func(Of T))(rewriter.Visit(this.expression)).Compile();
                                }
                                return this.func();
                                }

                                Рерайтер внутри себя, по сути, разворачивает стартовое выражение в енумерабл.

                                Для тех, кто в танке, да.
                            +1
                            Собака зарыта в смешении ответственностей.

                            Определите, как минимум:
                            1) кто отвечает за трансляцию из одной модели в другую. Пусть эта сущность больше ничем не занимается, тогда у неё будет своё имя, а helper'ы больше не будут вызывать вопросов.
                            2) кто отвечает за работу с DataContext и как он это будет делать
                            2.1) будет ли он получать фильтр для данных (пользователей) извне или сам будет строить Expression<Func<T, bool>>
                            2.2) в каком виде он будет возвращать данные — готовые коллекции/списки или IQueryable

                            По примерам кода можно предположить, что нету стремления получать именно перечисления в GetUsers, но есть желание получать фильтрованный список пользователей. Тогда IEnumerable вычёркиваем.
                            IQueryable тоже вычёркиваем — извне не используете, тогда зачем его возвращать? Возвращайте готовую коллекцию/список.

                            Лишнее наследование UserRepository: MyProjectDataContext можно заменить разделением ответственностей по классам либо создав нужные методы DataContext'а в partial классе.
                              0
                              Вот уж согласен, DataContext, как минимум, должен быть внутри Repository, в виде переменной-источника данных. А чтобы избежать всех этих проекций в коде, пытаясь попутно исправить «impedance mismatch», лучше использовать Entity Framework или NHibernate, чтобы сгрузить маппинг на них. А проекции для ViewModel'ей можно легко создавать AutoMapper'ом.
                                +1
                                В этом топике я не стремился показать то как нужно проектировать архитектуру, это совершенно отдельная тема, примеры были собраны за 5 минут, чтобы продемонстрировать суть проблемы и ее решения. Я лишь хотел обратить внимание разработчиков, на некоторые тонкости, которые явно не бросаются в глаза. перед тем как написать, я пообщался со знакомыми — разработчиками разного уровня. В результате получился следующий: для 5-ых это было реальной новостью, действительно об этом не нюансе знали. 2-е наиболее опытных ответили что хорошо знакомы с такой проблемой, и знают о чем говорят. 1 из них — ostapkoenig, дал очень дельное замечание по оптимизации, хотя и выходящее за рамки темы статьи.
                                –4
                                В упор не могу понять, почему SQL-логику не оставить базе данных, т.е. почему бы не использовать хранимые процедуры для этой выборки, а в GetUsers() вызывать эту процедуру и потом возвращать коллекцию результатов? Мне интересно, почему автор не решил использовать этот способ, ведь L2S позволяет очень легко вызывать хранимые процедуры из кода программы.
                                  0
                                  Не холивара ради, но:
                                  ormeter.net/

                                  Мы выбрали для себя DataObjects.net
                                    0
                                    Кстати, почему выбрали?
                                      +1
                                      Рассказывать очень долго, т.к. выбирали тоже долго :)
                                      В двух словах:
                                      очень удачно и удобно сочетается с DDD, хорошо поддерживает LINQ (ну или процедурный стиль), при этом
                                      быстро работает, хороший саппорт.

                                      Рекомендую почитать немного документацию, чтобы понять подход, а также
                                      принципы сущностей. Разработчики считают, что Persistence Ignorance
                                      не всегда удачная модель и использую т.н. Persistence Awareness.

                                      Вот хорошая статья о принципиальной разнице между NHibernate и DataObjects.
                                      blog.dataobjects.net/2010/11/dataobjectsnet-vs-nhibernate-conceptual.html

                                      Да, и еще — разработчики с Екатеринбурга :)

                                  Only users with full accounts can post comments. Log in, please.