Комментарии 35
Привет!
Всё верно. Добавлю: я в своих проектах стараюсь абсолютно все linq-запросы оформлять в виде скомпилированных expressions. Делается это при помощи хэлпера CompiledQuery.Compile(). Это позволяет избежать путаницы (зачастую из-за недостатка должного внимания), а так же значительно увеличивает производительность выполнения запроса.
Всё верно. Добавлю: я в своих проектах стараюсь абсолютно все linq-запросы оформлять в виде скомпилированных expressions. Делается это при помощи хэлпера CompiledQuery.Compile(). Это позволяет избежать путаницы (зачастую из-за недостатка должного внимания), а так же значительно увеличивает производительность выполнения запроса.
Да ты прав, тоже очень полезная возможность для оптимизации.
НЛО прилетело и опубликовало эту надпись здесь
Да, хорошо. Однако я использую не LinqToSql, а EntityFramework в .net 4.0. Привет!
НЛО прилетело и опубликовало эту надпись здесь
http://habrahabr.ru/blogs/net/47336/
Возможно будет полезным — когда-то писал
Возможно будет полезным — когда-то писал
Такое ощущение, что вы не шарите, почему в Linq2Sql используется Expression<Func<..>>, а не Func<..>
Я очень рад что вы, уважаемый в этом шарите. Но вы в этом не одиноки. Есть и другие избранные, которые понимают магию Expression<Func<..>> и умеют ее использовать :)
Ммм. Отлично. Было бы еще лчше, если бы другие избранные, про которых вы говорите, вам с самого начала пояснили, почему в вашем случае linq-запрос не достраивался, а распадался на sql-objects-sql, потому что такую «засаду» вы выяснили чисто случайно :) Надеюсь, в вашем проекте это было единственное место.
P.S. У нас бы за такое в процессе codereview погнали бы ссаными тряпками.
P.S. У нас бы за такое в процессе codereview погнали бы ссаными тряпками.
Ну так ждем от вас доступного объяснения. Давайте, доступное объяснение в студию!
Как тока мне вернут мою карму на место, так сразу… Иш, потянулись уже ручонки у кого-то.
Шутка.
На самом деле, вы уже написали половину ответа.
Тот Select, который вы делаете по IEnumerable[T], требует наличия сущностей в памяти. Поэтому IQueryable приводится к IEnumerable (своего рода слайсинг) единственным доступным ему способом — он делает то, что в ORM называется материализацией сущностей. После чего навигационный доступ в памяти по одному отправляет запросы в БД за параметрами.
Во втором случае IQueryable[T] комбинирует запрос, достраивая join. Потому что он знает целевой язык (sql), и потому что результаты первой части запроса еще не материализованы, а представлены в виде Expression[Func[T]].
Я кончил (доклад) и закурил
Шутка.
На самом деле, вы уже написали половину ответа.
Тот Select, который вы делаете по IEnumerable[T], требует наличия сущностей в памяти. Поэтому IQueryable приводится к IEnumerable (своего рода слайсинг) единственным доступным ему способом — он делает то, что в ORM называется материализацией сущностей. После чего навигационный доступ в памяти по одному отправляет запросы в БД за параметрами.
Во втором случае IQueryable[T] комбинирует запрос, достраивая join. Потому что он знает целевой язык (sql), и потому что результаты первой части запроса еще не материализованы, а представлены в виде Expression[Func[T]].
Я кончил (доклад) и закурил
Ваш код эквивалентен следующему:
Происходит первый вызов в строке (1) и потом еще 10 раз в строке (2)
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)
НЛО прилетело и опубликовало эту надпись здесь
Хех, согласен на миллион процентов. Дело в том, что LINQ 2 SQL — это и не реализация паттерна ActiveRecord, где можно было бы со спокойной душой забыть про слово репозиторий и позволить сущностям самим знать все о доступе к данным (без DataContext). И, тем более, L2S — никаким боком и рядом не стоит с парадигмами DDD (Domain-Driven-Design), чтобы пытаться им следовать.
Вот и получаются всякие RepositoryHelper'ы и HelperRepository'и :)
Вот и получаются всякие RepositoryHelper'ы и HelperRepository'и :)
Мне кажется, что речь была не про «забыть про слово репозиторий», про концовку в виде «Helper».
Просто ходят слухи, то полезно называть класс так, чтоб он описывал то, что делает. К примеру ModelUserProvider/Convertor.
Часто советуют избегать приставок из серии Heler/Utils аргументируя тем, что это смахивает «процедурным программированием» и не отображает своей сути.
Не знаю, правда это или нет, но вот есть такое мнение («и не только мое» (с))
Просто ходят слухи, то полезно называть класс так, чтоб он описывал то, что делает. К примеру ModelUserProvider/Convertor.
Часто советуют избегать приставок из серии Heler/Utils аргументируя тем, что это смахивает «процедурным программированием» и не отображает своей сути.
Не знаю, правда это или нет, но вот есть такое мнение («и не только мое» (с))
Нет, обычно в .NET используется название Helper, чтобы заключить в этот класс дополнительный функционал, построенный поверх уже существующего в основном классе, чтобы не перегружать последний. Extension-методы в .NET — по сути, созданы для той же цели.
Также бывает, что Helper-классы содержат некоторую логику, не имеющую прямого отношения к тому, что делает сам объект (обычно это «аспекты»/AOP). Впрочем, в этом случае, логику содержат не сами эти Helper-классы, а другие классы, используемые внутри хелперов.
Приведенный пример в статье, с RepositoryHelper, дейстительно, не отражает сути того, что он делает, и более того — содержит логику, которая должна, по идее быть либо в репозитории, либо в слое сервисов (Application Services).
Также бывает, что Helper-классы содержат некоторую логику, не имеющую прямого отношения к тому, что делает сам объект (обычно это «аспекты»/AOP). Впрочем, в этом случае, логику содержат не сами эти Helper-классы, а другие классы, используемые внутри хелперов.
Приведенный пример в статье, с RepositoryHelper, дейстительно, не отражает сути того, что он делает, и более того — содержит логику, которая должна, по идее быть либо в репозитории, либо в слое сервисов (Application Services).
Да это все ясно, но вот как мне кажется, суть комментария hazzik в том, что возможно стоит называть классы именно так, чтоб названия описывали то, что они делают. От «расширителей», «помощников», «фасадов» и т.д. все равно никуда не денемся.
Просто когда немного поработаешь в крупных проектах, то столько насмотришься на всякие Utils/Helper-а что невольно начинает типать когда смотришь на них. При чем могут быть десятки классов с одним названием (типа PageHelper), содержащий совершенно разных «вспомогательный» функционал.
В прочем, nevermind, думаю поднятая тема никак не касается именования классов.
Просто когда немного поработаешь в крупных проектах, то столько насмотришься на всякие Utils/Helper-а что невольно начинает типать когда смотришь на них. При чем могут быть десятки классов с одним названием (типа PageHelper), содержащий совершенно разных «вспомогательный» функционал.
В прочем, nevermind, думаю поднятая тема никак не касается именования классов.
Это уже работая над «очередным» проектом, вы только столкнулись с сутью проблемы SELECT N+1? :)
Очевидно, что автор не в теме.
dmitry_h, честно, не создается впечатление о том, что вы разбираетесь в этом. Не обижайтесь, здесь мы для того, чтобы помогать друг другу. Советую вам ознакомиться с книгой:
LINQ. Карманный справочник. Албахари Б. Албахари Д.
Бумажный вариант:
shop.top-kniga.ru/books/item/in/395268/
Электронный:
www.twirpx.com/file/156984/
LINQ. Карманный справочник. Албахари Б. Албахари Д.
Бумажный вариант:
shop.top-kniga.ru/books/item/in/395268/
Электронный:
www.twirpx.com/file/156984/
Как вам верно написали выше, вы просто не понимаете, как внутри себя работает Linq2Sql. А никакого (ровным счетом никакого) отношения к интерфейсам IEnumerable/IQueryable описанная вами проблема не имеет. Если вы решите просто привести одно к другому, запросов в базу это не уменьшит.
Потому что, вы не поверите, стоит при создании датаконтекста указать правильные LoadOptions, как в первом же вашем примере будет один запрос, а не N+1 (вне зависимости от всех ваших измышлений с типами).
Потому что, вы не поверите, стоит при создании датаконтекста указать правильные LoadOptions, как в первом же вашем примере будет один запрос, а не N+1 (вне зависимости от всех ваших измышлений с типами).
На всякий случай проиллюстрирую мысль «Если вы решите просто привести одно к другому, запросов в базу это не уменьшит.»
Что делает вызов Enumerable.Select(source, selector)? Если выкинуть обработку параметров, то он сведется к return WhereSelectEnumerableIterator(source, null, selector);
Что делает вызов Enumerable.Select(source, selector)? Если выкинуть обработку параметров, то он сведется к return WhereSelectEnumerableIterator(source, null, selector);
(… тьфу ты, продолжаем)
Что делает вызов Queryable.Select(source, selector)? Создает Expression, который описывает… вызов метода Select на source (с параметром selector). Кем будет выполнен этот expression? QueryProvider-ом, заданный в source (а source, напомню, пришел к нам из AsQueryable). А вот теперь посмотрим на AsQueryable: EnumerableQuery.Create(..., source), что, в свою очередь, дает нам создание EnumerableQuery(Of T), которая является провайдером сама для себя. Что делает ее Execute? В итоге, после множества перенаправлений, выясняется, что он всего лишь компилирует expression и вызывает его.
Вот и все.
Подводя итог: AsQueryable — это всего лишь враппер, работа которого все равно сведется к вызову оригинальных методов на Enumerable.
Что делает вызов Queryable.Select(source, selector)? Создает Expression, который описывает… вызов метода Select на source (с параметром selector). Кем будет выполнен этот expression? QueryProvider-ом, заданный в source (а source, напомню, пришел к нам из AsQueryable). А вот теперь посмотрим на AsQueryable: EnumerableQuery.Create(..., source), что, в свою очередь, дает нам создание EnumerableQuery(Of T), которая является провайдером сама для себя. Что делает ее Execute? В итоге, после множества перенаправлений, выясняется, что он всего лишь компилирует expression и вызывает его.
Вот и все.
Подводя итог: AsQueryable — это всего лишь враппер, работа которого все равно сведется к вызову оригинальных методов на Enumerable.
Для тех кто в танке:
Так выглядит метод статического класса 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.
Так выглядит метод статического класса 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.
Ох. Вы мой коммент-то прочитали? 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();
}
Рерайтер внутри себя, по сути, разворачивает стартовое выражение в енумерабл.
Для тех, кто в танке, да.
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) кто отвечает за трансляцию из одной модели в другую. Пусть эта сущность больше ничем не занимается, тогда у неё будет своё имя, а helper'ы больше не будут вызывать вопросов.
2) кто отвечает за работу с DataContext и как он это будет делать
2.1) будет ли он получать фильтр для данных (пользователей) извне или сам будет строить Expression<Func<T, bool>>
2.2) в каком виде он будет возвращать данные — готовые коллекции/списки или IQueryable
По примерам кода можно предположить, что нету стремления получать именно перечисления в GetUsers, но есть желание получать фильтрованный список пользователей. Тогда IEnumerable вычёркиваем.
IQueryable тоже вычёркиваем — извне не используете, тогда зачем его возвращать? Возвращайте готовую коллекцию/список.
Лишнее наследование UserRepository: MyProjectDataContext можно заменить разделением ответственностей по классам либо создав нужные методы DataContext'а в partial классе.
Определите, как минимум:
1) кто отвечает за трансляцию из одной модели в другую. Пусть эта сущность больше ничем не занимается, тогда у неё будет своё имя, а helper'ы больше не будут вызывать вопросов.
2) кто отвечает за работу с DataContext и как он это будет делать
2.1) будет ли он получать фильтр для данных (пользователей) извне или сам будет строить Expression<Func<T, bool>>
2.2) в каком виде он будет возвращать данные — готовые коллекции/списки или IQueryable
По примерам кода можно предположить, что нету стремления получать именно перечисления в GetUsers, но есть желание получать фильтрованный список пользователей. Тогда IEnumerable вычёркиваем.
IQueryable тоже вычёркиваем — извне не используете, тогда зачем его возвращать? Возвращайте готовую коллекцию/список.
Лишнее наследование UserRepository: MyProjectDataContext можно заменить разделением ответственностей по классам либо создав нужные методы DataContext'а в partial классе.
Вот уж согласен, DataContext, как минимум, должен быть внутри Repository, в виде переменной-источника данных. А чтобы избежать всех этих проекций в коде, пытаясь попутно исправить «impedance mismatch», лучше использовать Entity Framework или NHibernate, чтобы сгрузить маппинг на них. А проекции для ViewModel'ей можно легко создавать AutoMapper'ом.
В этом топике я не стремился показать то как нужно проектировать архитектуру, это совершенно отдельная тема, примеры были собраны за 5 минут, чтобы продемонстрировать суть проблемы и ее решения. Я лишь хотел обратить внимание разработчиков, на некоторые тонкости, которые явно не бросаются в глаза. перед тем как написать, я пообщался со знакомыми — разработчиками разного уровня. В результате получился следующий: для 5-ых это было реальной новостью, действительно об этом не нюансе знали. 2-е наиболее опытных ответили что хорошо знакомы с такой проблемой, и знают о чем говорят. 1 из них — ostapkoenig, дал очень дельное замечание по оптимизации, хотя и выходящее за рамки темы статьи.
В упор не могу понять, почему SQL-логику не оставить базе данных, т.е. почему бы не использовать хранимые процедуры для этой выборки, а в GetUsers() вызывать эту процедуру и потом возвращать коллекцию результатов? Мне интересно, почему автор не решил использовать этот способ, ведь L2S позволяет очень легко вызывать хранимые процедуры из кода программы.
НЛО прилетело и опубликовало эту надпись здесь
Рассказывать очень долго, т.к. выбирали тоже долго :)
В двух словах:
очень удачно и удобно сочетается с DDD, хорошо поддерживает LINQ (ну или процедурный стиль), при этом
быстро работает, хороший саппорт.
Рекомендую почитать немного документацию, чтобы понять подход, а также
принципы сущностей. Разработчики считают, что Persistence Ignorance
не всегда удачная модель и использую т.н. Persistence Awareness.
Вот хорошая статья о принципиальной разнице между NHibernate и DataObjects.
blog.dataobjects.net/2010/11/dataobjectsnet-vs-nhibernate-conceptual.html
Да, и еще — разработчики с Екатеринбурга :)
В двух словах:
очень удачно и удобно сочетается с DDD, хорошо поддерживает LINQ (ну или процедурный стиль), при этом
быстро работает, хороший саппорт.
Рекомендую почитать немного документацию, чтобы понять подход, а также
принципы сущностей. Разработчики считают, что Persistence Ignorance
не всегда удачная модель и использую т.н. Persistence Awareness.
Вот хорошая статья о принципиальной разнице между NHibernate и DataObjects.
blog.dataobjects.net/2010/11/dataobjectsnet-vs-nhibernate-conceptual.html
Да, и еще — разработчики с Екатеринбурга :)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
IEnumerable<T> и IQueryable<T>, в чем разница?