Еще один взгляд на Entity Framework: производительность и подводные камни

Ни для кого не секрет, что адаптация Entity Framework проходит очень медленно. Огромное количество компаний продолжают использовать Linq2Sql и не планируют менять его на что-то новое в обозримом будущем, несмотря на то, что EF – официально рекомендуемая Microsoft технология доступа к БД, а Linq2Sql уже почти не поддерживается.

Тех, кто всё еще сомневается, можно ли использовать EF (и особенно – code first) на реальных проектах, приглашаю под кат.

Введение


Во-первых, если у вас за плечами уже пара выполненных проектов с использованием EF – дальше можно не читать. Вряд ли вы узнаете что-то новое.

Несмотря на то, что ниже будет довольно много отсылок на Linq2Sql, пост не является прямым сравнением технологий. Я банально не знаю Linq2Sql достаточно хорошо, чтобы адекватно его сравнивать с чем-нибудь.

Это и не мануал по оптимизации, хотя некоторые рекомендации будут даны. Полезный материал может быть найден здесь или тут. Также не будут рассмотрены методы оптимизации через написанный руками SQL-код, хотя его рано или поздно придется писать в любом более-менее крупном проекте.

Пост предполагает хотя бы общее знакомство с Entity Framework Code First, SQL и вообще принципами ORM.

Актуальной версией EF на момент написания является EF5. Для получения максимальной производительности был использован .NET 4.5. Все, сказанное ниже, относится в первую очередь к Code First с автоматической генерацией БД (который активно рекламируется самими сотрудниками Microsoft). Сервер БД – LocalDb (обычно работает медленнее, чем полноценный MS SQL Server).

Тестовый код доступен на GitHub.

Модель данных


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

У каждого пользователя (Account) есть несколько папок с письмами (MessageFolder). В каждой папке хранится несколько цепочек писем (MessageThread), при этом одна цепочка может находиться в множестве папок одновременно (ThreadsInFolders). Каждая цепочка состоит из множества писем. При этом цепочки шарятся между аккаунтами, письма – нет.

Само собой, модель в подобном виде вряд ли может быть использована в реальном коде (хотя, несомненно, что-нибудь в духе может встретиться), но это была первая маленькая, но достаточно сложная для наших экспериментов система, пришедшая мне в голову. Извините.

Она же в коде
public class Account
    {
        public int Id { get; set; }
        [Required]
        public string Name { get; set; }
    }

    public class Message
    {
        public int Id { get; set; }
        public bool IsRead { get; set; }
        public DateTime Date { get; set; }
        public string Text { get; set; }

        [Required]
        public virtual MessageThread Thread { get; set; }
        [Required]
        public virtual Account Owner { get; set; }
        [Required]
        public virtual Account Sender { get; set; }
        [Required]
        public virtual Account Receiver { get; set; }
    }

    public class MessageThread
    {
        public int Id { get; set; }
        [Required]
        [StringLength(150)]
        public string Subject { get; set; }

        public virtual ICollection<MessageFolder> Folders { get; set; }
    }

    public class MessageFolder
    {
        public int Id { get; set; }
        public string Name { get; set; }
        [Required]
        public Account Owner { get; set; }

        public virtual ICollection<MessageThread> Threads { get; set; }
    }

Чтение данных


Самым главным в любой ORM-системе является чтение данных. Тут у EF проблем нет – запросы к БД ходят весело и задорно, поддерживается ленивая загрузка. Все загруженные объекты попадают во внутренний кэш контекста, затем могут быть запрошены напрямую из него (только через метод Find).

Простенький запрос будет выглядеть примерно следующим образом:
context.Messages.AsNoTracking().OrderBy(f => f.Id).Take(count)
                .Include(f => f.Owner).Include(f => f.Sender).Include(f => f.Receiver)
                .ToList();

SQL
SELECT TOP (100) 
[Extent1].[Id] AS [Id], 
[Extent1].[IsRead] AS [IsRead], 
[Extent1].[Date] AS [Date], 
[Extent1].[Text] AS [Text], 
[Extent2].[Id] AS [Id1], 
[Extent2].[Name] AS [Name], 
[Extent3].[Id] AS [Id2], 
[Extent3].[Name] AS [Name1], 
[Extent4].[Id] AS [Id3], 
[Extent4].[Name] AS [Name2]
FROM    [dbo].[Message] AS [Extent1]
INNER JOIN [dbo].[Account] AS [Extent2] ON [Extent1].[Owner_Id] = [Extent2].[Id]
INNER JOIN [dbo].[Account] AS [Extent3] ON [Extent1].[Sender_Id] = [Extent3].[Id]
INNER JOIN [dbo].[Account] AS [Extent4] ON [Extent1].[Receiver_Id] = [Extent4].[Id]
ORDER BY [Extent1].[Id] ASC

AsNoTracking отключает слежение EF за получаемыми объектами (они не будут кэшированы); Include – подгружает указанные связанные сущности (navigation property/свойства навигации) через генерацию, чаще всего, INNER JOIN; остальное – знакомый всем LINQ.

Самый интересный момент здесь – Include. Строго говоря, окончательное осознание того, что вам может понадобиться, происходит на уровне презентации, поэтому самое логичное расположение всех Include – там, либо на соседнем уровне (например, контроллеров в MVC). Фактически это означает, что вам придется протаскивать IQueryable через всю структуру приложения, постоянно следя, чтобы никто не использовал методы, несовместимые с Linq2Entities, иначе это выльется в исключение. С классической архитектурой (репозитории-сервисы-контроллеры) такой подход мало приятен. Возможно, организация домена через запросы или extension methods к IQueryable поможет с этим, но я не пробовал.

Любой запрос кроме Find
ходит к БД. Find делает это лишь в том случае, если сущность с необходимыми ключевыми полями не была найдена в локальном кэше контекста. То есть, из двух вызовов context.Messages.Find(1) на одном контексте, лишь первый из них выльется в запрос к БД. То же касается загрузки связанных сущностей. Например, в приведенном ниже коде владелец сообщения будет получен из кэша.
context.Accounts.Load(); //аккаунты были загружены в кэш где-нибудь в другом месте
var m = context.Messages.First();
Console.WriteLine(m.Owner.Name);

SQL
SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name]
FROM [dbo].[Account] AS [Extent1]
GO

SELECT TOP (1) 
[c].[Id] AS [Id], 
[c].[IsRead] AS [IsRead], 
[c].[Date] AS [Date],
[c].[Text] AS [Text], 
[c].[Thread_Id] AS [Thread_Id], 
[c].[Owner_Id] AS [Owner_Id], 
[c].[Sender_Id] AS [Sender_Id], 
[c].[Receiver_Id] AS [Receiver_Id]
FROM [dbo].[Message] AS [c]

Вполне логичный context.Messages.Include(f=>f.Owner).First() для избегания ленивой загрузки здесь бы лишь снизил производительность. Вывод: не стоит городить Include без предварительного изучения SQL-запросов, отправляемых к серверу.

Кроме того, если по условию задачи необходимо, чтобы в результаты попадали добавленные, но еще не сохраненные в БД данные, необходимо будет вручную проводить запрос к context.Set().Local, а затем объединять его с данными, полученными из БД. Либо вызывать SaveChanges заранее. Еще один аргумент в пользу короткого времени жизни контекстов. Тут полная аналогия с Linq2Sql.

По поводу производительности: первый приведенный в этой части запрос для 1000 записей выполняется EF примерно в 2-3 раза медленнее, чем Linq2Sql. Однако, API загрузки намного приятнее, чем аналогичное в Linq2Sql.

Добавление данных


Добавление данных реализовано довольно просто. Любая добавленная через метод Add сущность просто добавляется во внутренний кэш контекста со статусом Added. При последующем вызове SubmitChanges проходит валидация, для всех подобных сущностей генерируются INSERT и они добавляются в БД. Все связанные navigation property, которые не были явно добавлены в контекст, так же принимаются как Added и добавляются в БД (что может служить источником довольно внезапных багов).

Основные советы по добавлению:
  1. При добавлении множества данных стоит отключить ValidateOnSaveEnabled и AutoDetectChangesEnabled. Это значительно поднимет производительность.
  2. Не стоит добавлять в одном контексте слишком много записей. Поскольку все они перед сохранением кэшируются в нем, большое количество объектов очень быстро вызывают проблемы с GC и количеством потребляемой памяти. В среднем, лучше пересоздавать контекст после каждых 100 записей.

Более подробно можно почитать на StackOverflow.
В плане производительность EF примерно равен, а то и обгоняет (после оптимизаций) Linq2Sql. Хотя, повторюсь, я не большой знаток Linq2Sql.

Модификация и удаление данных


Эти операции традиционно являются слабым место .NET ORM-ок. Главной проблемой является отсутствие поддержки Bulk-операций. То, что в SQL легко выражается через один запрос, ORM может сделать за тысячу.
Допустим, необходимо пометить все сообщения в цепочке как прочитанные. В EF это будет выглядеть подобным образом:
var messages = context.Messages.Where(f=>f.Thread.Id == threadId);
foreach (var m in messages)
    m.IsRead = true;
context.SaveChanges();

SQL
SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[IsRead] AS [IsRead], 
[Extent1].[Date] AS [Date], 
[Extent1].[Text] AS [Text], 
[Extent1].[Thread_Id] AS [Thread_Id], 
[Extent1].[Owner_Id] AS [Owner_Id], 
[Extent1].[Sender_Id] AS [Sender_Id], 
[Extent1].[Reciever_Id] AS [Reciever_Id]
FROM [dbo].[Message] AS [Extent1]
WHERE [Extent1].[Thread_Id] = @p__linq__0

update [dbo].[Message]
set [IsRead] = @0
where ([Id] = @1)
--(N раз)

Во-первых, это вызовет загрузку всех модифицируемых записей в контекст, а затем генерацию отдельного запроса для каждой из них. Большое количество сущностей, несомненно, обрадует сборщик мусора, а большое количество запросов – сервер БД. Кроме того, существуют некоторые проблемы с валидацией, которые будут описаны ниже. Удаление организуется похожим образом и имеет те же проблемы с производительностью.

Причины отсутствия множественных update/delete лично мне непонятны. И не только мне, поэтому существуют подобные мануалы и сторонние библиотеки.

С EntityFramework.Extended приведенный выше код будет выглядеть следующим образом:
var messages = context.Messages.Where(f=>f.Thread.Id == threadId);
messages.Update(f => new Message {IsRead = true});

SQL
UPDATE [dbo].[Message] SET 
[IsRead] = @p__update__0 
FROM [dbo].[Message] AS j0 INNER JOIN (
SELECT 
[Extent1].[Id] AS [Id]
FROM [dbo].[Message] AS [Extent1]
WHERE [Extent1].[Thread_Id] = @p__linq__0
) AS j1 ON (j0.[Id] = j1.[Id])

Код выполняет всего один запрос к базе данных. Обратите внимание, что нет вызова SaveChanges – SQL выполняется сразу в обход контекста. Это может привести к некоторым неочевидным багам, когда контекст будет содержать данные о какой-либо записи, которая в действительности будет удалена/изменена.

Кроме того, библиотека генерирует не самый эффективные запросы, внутри довольно сильно опирается на dynamic и косвенно генерирует неимоверное количество исключений внутри фрэймворка (их видно, например, в IntelliTrace), тем самым представляя значительную базовую стоимость одного запроса. Пример в таблице ниже (EntityFramework без валидации).
Способ Модификация 10 записей, мс Модификация 10000 записей, мс
EntityFramework 8 935
EntityFramework.Extended 28 83

В общем случае, использование EntityFramework.Extended оправдано для большого количества данных, при небольшом лучше обойтись без него. Но всё это микрооптимизации.

Коллекции как navigation property


От заказчика приходит просьба – давайте показывать возле имени каждой папки сообщений количество цепочек в ней. «Без проблем!» отвечает программист и быстро добавляет следующий код:
var count = folder.Threads.Count();

Как же хорошо, что существуют navigation properties, думает он, пока не видит генерируемый SQL.
SQL
SELECT 
[Extent2].[Id] AS [Id], 
[Extent2].[Subject] AS [Subject]
FROM  [dbo].[ThreadsInFolders] AS [Extent1]
INNER JOIN [dbo].[MessageThread] AS [Extent2] ON [Extent1].[MessageThread_Id] = [Extent2].[Id]
WHERE [Extent1].[MessageFolder_Id] = @EntityKeyValue1

Что в EF, что в Linq2Sql, вызов Count(), Any(), да и вообще любого запроса на коллекции, используемой как navigation property, приводит в полной загрузке этой коллекции в память. Причины можно понять, но об этой «фиче» зачастую забывают. Корректный запрос, считающий количество на уровне БД, выглядит так:
var count = context.Threads.Count(f => f.Folders.Any(e => e.Id == folder.Id)));

SQL
SELECT 
[GroupBy1].[A1] AS [C1]
FROM ( SELECT 
	COUNT(1) AS [A1]
	FROM [dbo].[MessageThread] AS [Extent1]
	WHERE  EXISTS (SELECT 
		1 AS [C1]
		FROM [dbo].[ThreadsInFolders] AS [Extent2]
		WHERE ([Extent1].[Id] = [Extent2].[MessageThread_Id]) AND ([Extent2].[MessageFolder_Id] = @p__linq__0)
	)
)  AS [GroupBy1]

Но он требует, чтобы цепочки имели ссылку на папки, в которых они хранятся, что, с точки зрения нашей архитектуры, немного «не православный ООП».

Даты


Недавно на одном из наших проектов обнаружился интересный баг – на одном из серверов падало кэширование любых картинок, хранящихся в БД.

Исключение генерировалось на следующей строке:
Response.Cache.SetLastModified(lastUpdated);

где lastUpdated – дата последней модификации картинки, получаемая прямо из БД.
В процессе дебага выяснилось, что ASP.NET MVC внутри себя вызывал на полученной дате ToUniversalTime(), и затем падал на проверке:
if (utcDate > DateTime.UtcNow) {
                throw new ArgumentOutOfRangeException("utcDate"); 
            }

Причина бага довольно проста – при сохранении в БД, DateTime теряет значение свойства Kind и при получении данных из БД получает DateTimeKind.Undefined (логичное значение, придуманное как раз для таких случаев, хотя лично я считаю хранение чего-либо кроме UTC в БД моветоном). Если вы используете даты только в собственном, полностью контролируемом коде, то это не имеет особого значения (поскольку большинство операций не учитывают DateTimeKind), но любой вызов ToUniversalTime() или ToLocalTime() сдвинет это время в зависимости от локальных настроек системы. Так, наше кэширование падало только если часовой пояс был GMT - N.

Подобное поведение характерно как для EF, так и для Linq2Sql и, строго говоря, никак не настраивается. Существуют разнообразные способы решения проблемы, но пользователям code first придется туго. Решение через подписку на событие ObjectMaterialized может отсрочить уничтожение контекста (если оно производится не руками) и привести к совсем неочевидным багам. Есть еще возможность использовать простенький враппер вокруг DateTime, и маппить его в таблицу как ComplexType, но придется наслаждаться всеми прелестями даты как reference type.

RequiredAttribute


Мой любимый подводный камень и главная причина моего разочарования в EF.

Напомню, что в сущности письма свойства Owner, Sender, Receiver и Thread помечены как Required. Во-первых, это указывает генератору БД на то, что поля должны быть помечены как NOT NULL, а во-вторых, включает на них внутреннюю валидацию EF.

Что, по-вашему, выполнит следующий код при включенной валидации на только что созданном контексте?
var message = context.Messages.Find(1);
message.IsRead = true;
context.SaveChanges();

Если вы решили, что он отметит письмо как прочитанное - вы ошиблись. Появится DbEntityValidationException.

Замечательный RequiredAttribute выдает ошибку валидации, ведь все помеченные им поля были равны null, а выполнять их ленивую загрузку он не умеет. Проблема обсуждалась, например, тут.

Путей решения несколько: можно подгружать все значения заранее (сложно), написать свой атрибут (велосипедно), использовать try-catch вокруг SaveChanges и подгружать все связи в catch (медленно), отключать валидацию, когда надо (опасно), а можно просто отказаться от RequiredAttribute и определить NOT NULL через Fluent API. Последнее, правда, сменит тип исключения, выбрасываемого при попытке добавить новую запись с кривыми данными, отключит валидацию на уровне кода, да и внесет довольно неочевидное поведение в модификацию существующих записей (любая установка Required свойств в null просто не будет попадать в UPDATE запрос). В любом случае, если эта проблема обнаружилась довольно поздно в цикле разработке, приятной она не окажется.

Выводы (tl;dr)


Можно ли использовать EF для чего-то кроме блога с одним посещением в месяц? Решать вам. Всё не так радужно, как Microsoft пытается нам рассказать и любой более-менее нагруженный проект потребует значительных усилий по оптимизации и исправлению недостатков самого EF.

В любом случае, оптимизацию доступа к БД через EF необходимо закладывать в архитектуру приложения изначально, а не только в момент, когда всё окажется слишком медленным. Легко может получиться так, что вашу могучую систему абстракций придется упрощать, либо вставлять большое количество костылей и некогда красивая система превратится в неподдерживаемого монстра, в каждом уголке которого будет очень жесткая зависимость на EF.

Есть ли преимущества у EF по сравнению с Linq2Sql? Решать, опять же, вам. Для меня главным преимуществом является возможность автогенерации БД через code first (он и правда очень приятен), но количество его ограничений на данный момент не поддается счету. Нет адекватной поддержки представлений, хранимых процедур и функций (должны поправить в EF6). Невозможно адекватно использовать в коде автоматически сгенерированную таблицу для связи много-ко-многим (можно определить её в коде самостоятельно, но это значительно убавит удобство работы с собственными классами) и т.д. Идеальных ORM-средств не бывает и EF – не исключение.

Как бы то ни было, в следующий раз я дважды подумаю, прежде чем посоветовать кому-либо Entity Framework. И больше никогда не буду верить на слово/график/сэмпл работникам, хвалящим продукт своей компании. Чего и вам желаю.
Поделиться публикацией

Похожие публикации

Комментарии 12

    +6
    После Нового Года честно-честно прочитаю! ))
    Спасибо Вам.
      0
      Занимаюсь разработкой open-source проекта с использование Entity Framework уже несколько лет. И могу сказать, что для меня (как и для сообщества проекта) самым большим недостатком является отсутствие официальной поддержки second level caching
        0
        Любопытно. А если сравнивать не с Linq2Sql, а с NHibernate, то что можно сказать?
          +3
          Если смотреть в перспективу, то EF выглядит получше.
          Microsoft имеет возможность модифицировать язык под фреймворк (ниже покажу на примере Expression Trees).

          Прежде всего, в MS думают о юзабилити.
          Если у db-класса Entity есть поле int EntityId, оно автоматически становится primary key, без дополнительных подсказок, если есть поле Order Order и поле int OrderId, фреймворк понимает, что OrderId — foreign Key для Order. В отличии от NH, EF не требует описания каждого поля как virtual, а только для lazy-полей.

          Я считаю, MS не ввело bulk update именно из-за того, что не придумало обобщённый изящный интерфейс, подходящий одинаково как к SQL, так и к коллекциям в памяти. А лучше ещё подумать, чем выкатить костыль, который из-за совместимости останется надолго во фреймворке.

          К слову, если в NH отмапить в одном entity одновременно и Order и OrderId, фреймворк будет падать где-то внутри с исключением «Индекс за пределами коллекции». Мне удалось выявить причину, только подключив исходники NH и подебажив их. Нужно одно из полей пометить как read-only (update=false, insert=false), чтобы можно было работать с таким маппингом.

          Запросы в EF строятся через знакомый всем linq, а не с использованием новых классов и новых методов, как в NHibernate (всякие ICriterion, DetachedCriteria, Restrictions и т.п.)
          Достаточно написать
          context.Orders.Where(order => order.Dealer.Kind == 1 || order.Dealer.Kind == 2)
          сравните с NH:
          var qOrders = DetachedCriteria.For<Order>();
          var qDealer = qOrders.CreateCriteria("Dealer");
          qDealer.Add(Restriction.Or(Restriction.Eq("Kind", 1), Restriction.Eq("Kind", 2));
          return session.List<Order>(qOrders);

          То есть, всё дерево условий строится вручную, а не пишется интуитивно понятными выражениями.

          Как видно из предыдущих примеров, NH ссылается на поля через их строковые имена, что противодействует автоматическому рефакторингу и поиску зависимостей. В инете описан велосипед указания полей через лямбду, но из-за reflection он небыстрый.

          К слову, ради EF в c# 4.0 ввели expression trees. Например, когда я хочу вызвать функцию получения списка заказов с указанием ключа сортировки в качестве параметра, вызывающий код выглядит как GetMyOrders(order => order.Date). Но это не лямбда (т.е. не Func<Order,object>). Это Expression Tree (тип которого Expression<Func<Order, object>>), парсинг происходит не в runtime, выковыриванием ссылки на поле из кода геттера, а в compile-time.

          Ещё о NH. Маппинги описываются в XML-аннотациях. Обычно, каждый entity-класс сопровождается xml-файлом с маппингом. Существует велосипед в community contrib делать маппинги атрибутами, который при запуске проекта рефлектит все классы в указанных сборках, делает XML из атрибутов и скармливает его ORM. Но не все конструкции, выражаемые в XML, можно описать атрибутами (хотя в одном проекте можно совмещать два подхода). В-общем, после NH переход на EF кажется очень приятным.

          Плюсом NH отмечу глубочайшую интеграцию с log4net. Если включить максимальный уровень логирования, в десятках мегабайт чтива можно увидеть всё, что происходит под капотом при выполнении небольшой функции. Кроме того, можно включить только логгер SQL, чего я не нашёл в EF (да, в EF любой IQueryable при вызове ToString() обычно даёт SQL, но как одним движениям залогировать все SQL-и, отсылаемые в БД, я не знаю).

          Из минусов EF отмечу катастрофичекое падение производительности при большом количестве объектов в контексте. Скорее всего, где-то зарылся небрежный алгоритм с O(N^2). Из-за чего приходится покрывать код, где речь о вставке хотя бы тысячи объектов, костылями типа
          context.AutoDetectChangesEnabled = prevEnabledValue;
          var prevEnabledValue = context.AutoDetectChangesEnabled;
          try { 
              context.AutoDetectChangesEnabled = false;
              ... 
          } finally { context.AutoDetectChangesEnabled = prevEnabledValue; }
          
            0
            В последнем куске кода первая строка лишняя.
            Кроме того, при выключении AutoDetectChangesEnabled, все изменённые объекты не сохраняются автоматически в БД при закрытии контекста (вероятно, требование NH описывать все поля в entity-классах, как virtual (даже простые int/string/date), не такое уж и глупое, т.к. позволяет легко отслеживать изменения в объектах)
              +1
              Из-за таких комментариев я люблю хабр, спасибо.

              А что вы скажете насчет Linq to NHibernate?
              при выключении AutoDetectChangesEnabled, все изменённые объекты не сохраняются автоматически в БД при закрытии контекста

              Маленькая поправка: при закрытии контекста никогда ничего автоматически не сохраняется. Необходимо вызвать SaveChanges().
              Кроме того, есть возможность вручную вызывать детект изменений через context.ChangeTracker.DetectChanges(). В зависимости от сложности модели, количестве объектов в контексте и характере операций, которые были выполнены, такой подход может поднять производительность. А может и значительно снизить.
                0
                >> Маленькая поправка: при закрытии контекста никогда ничего автоматически не сохраняется. Необходимо вызвать SaveChanges().

                Совершенно верно. У нас аспектное управление тразнакциями (стартует, если впервые входит в метод, помеченный атрибутом, коммитится при выходе из метода или роллбэчится при исключении), поэтому совершенно забыл, как надо закрывать транзакции.

                >> А что вы скажете насчет Linq to NHibernate?

                В принципе работает, запускали простые примеры. Но дальше примеров не пошло, т.к. решили, что нужен полноценный ORM.
              +1
              Microsoft имеет возможность модифицировать язык под фреймворк (ниже покажу на примере Expression Trees).

              То, что Microsoft может вводить в C# для поддержки EF, можно использовать и в NHibernate.
              Если у db-класса Entity есть поле int EntityId, оно автоматически становится primary key, без дополнительных подсказок, если есть поле Order Order и поле int OrderId, фреймворк понимает, что OrderId — foreign Key для Order.

              «Автоматическое» определение чего-либо обычно уж очень подозрительно и чревато граблями. Указать поле для ключа совсем не долго.
              В отличии от NH, EF не требует описания каждого поля как virtual, а только для lazy-полей.

              Вроде он требует описывание как virtual не просто так? А для автоматического отслеживания изменений.
              Запросы в EF строятся через знакомый всем linq, а не с использованием новых классов и новых методов, как в NHibernate (всякие ICriterion, DetachedCriteria, Restrictions и т. п.)

              Возможность описывать запрос linq-выражением была ещё во второй версии отдельной библиотекой. В третьей она уже включена.
              Ещё о NH. Маппинги описываются в XML-аннотациях. Обычно, каждый entity-класс сопровождается xml-файлом с маппингом. Существует велосипед в community contrib делать маппинги атрибутами, который при запуске проекта рефлектит все классы в указанных сборках, делает XML из атрибутов и скармливает его ORM. Но не все конструкции, выражаемые в XML, можно описать атрибутами (хотя в одном проекте можно совмещать два подхода). В-общем, после NH переход на EF кажется очень приятным.

              В третьей версии NH появилась возможность описывать маппинги кодом. Причём возможности там, похоже, абсолютно все (я не сталкивался с тем, что что-то можно описать xml, но нельзя кодом).
              Из минусов EF отмечу катастрофичекое падение производительности при большом количестве объектов в контексте.

              Аналогично, была программа, где NH был почти в 3 раза быстрее. Как я ни бился, но разогнать EF не удалось.

              Огромное преимущество и огромный недостаток NH по сравнению с EF в том, что он гораздо мощнее. И гораздо сложнее в освоении. Написать с нуля маппинг и сущности на NH почти на порядок дольше, чем на EF. Зато столько возможностей…
                0
                Справедливые замечания, мои впечатления относятся к версии NH 2.1.2.
            0
            =>Как же хорошо, что существуют navigation properties, думает он, пока не видит генерируемый SQL.
            Есть такой вопрос — а navigation property вообще получается нефункциональная фича? Попробовал несколько разных запросов, на примере folder.Threads.Where(f=>…
            Результат тот же самый, он полностью загружает коллекцию в память.
            То есть использовать можно только вверх, от многих к одному?

            И еще вопрос про методы обхода, почему вместо вот этого
            => var count = context.Threads.Count(f => f.Folders.Any(e => e.Id == folder.Id)));
            не использовать вот так?
            var count = context.Threads.Where(e=> e.id == folder.id).Count();

            у меня получился более простой запрос, сущности не буду тут подменять, as is
            SELECT
            COUNT(1) AS [A1]
            FROM [dbo].[SiteLink] AS [Extent1]
            WHERE [Extent1].[UserId] = @p__linq__0
              0
              Есть такой вопрос — а navigation property вообще получается нефункциональная фича?

              Почему, очень даже полезная, если работа и правда происходит со всеми элементами коллекции. Т.е. когда загрузка в память желательна — она делается просто. Когда не желательна — да, надо искать другие средства.

              var count = context.Threads.Where(e=> e.id == folder.id).Count();

              Вы сравниваете Id потока с Id папки — это баг.
              Ваш SQL работает для варианта 1 ко многим, а в задаче много ко многим.
                0
                Как то попутал со связями. Ну да ладно.

                Все равно как то непонятно, почему две записи условно одинаковы, возвращают один и тот же набор данных, получают их с сервера одним и тем же запросом, но на первую можно навесить Count, First, или еще что, и все это войдет в запрос и будет выполняться на сервере, а во втором случае все довески уже будут работать в памяти на загруженной коллекции
                context.SiteLink.Where(a => a.UserId == user.UserId)
                user.SiteLink

                Я правильно думаю, что неэквивалентность в том, что первый вариант дает «ленивую» коллекцию, которая материализуется при итерировании на ней, а второй вариант уже материализованную должен вернуть?

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое