Ускоряем Update и Delete операции для Entity Framework

от автора

У ORM Entity Framework есть Ахиллесова пята. Состоит она в том, что из CRUD операций только лишь Create и Read выполняются оптимально. Для Update и Delete в варианте из коробки мы сначала должны прочитать целиком запись из БД и лишь потом можем её обновить. И, да, чтобы удалить запись мы тоже должны её сначала прочитать.

Т.е. несчастный программист должен писать что-то вроде

using (var ctx = new ShopEntities()) {     foreach (var u in ctx.Users)     {         ctx.Users.Remove(u);     }     ctx.SaveChanges(); }

Но с выходом пакета EntityFramework.Extended ситуация в корне меняется.

Итак, устанавливаем пакет из репозитория командой “Install-Package EntityFramework.Extended”. Далее подключаем пространство имен “EntityFramework.Extensions”.
И начинается магия.

Теперь удаление выглядит вот так.

using (var ctx = new ShopEntities()) {     var itemsDeleted = ctx.Users.Delete(u => u.Orders.Count > 10);     //Осторожно, запрос уже улетел в БД      //и, несмотря на отсутствие вызова  ctx.SaveChanges(), данные были удалены     Console.WriteLine("{0} users were deleted", itemsDeleted); } 

Кстати, не лишним будет посмотреть, что полетело на сервер.
Это был вот такой запрос

DELETE [dbo].[Users] FROM [dbo].[Users] AS j0 INNER JOIN ( SELECT      [Project1].[ID] AS [ID]     FROM ( SELECT          [Extent1].[ID] AS [ID],          (SELECT              COUNT(1) AS [A1]             FROM [dbo].[Orders] AS [Extent2]             WHERE [Extent1].[ID] = [Extent2].[UserID]) AS [C1]         FROM [dbo].[Users] AS [Extent1]     )  AS [Project1]     WHERE [Project1].[C1] > 10 ) AS j1 ON (j0.[ID] = j1.[ID]) go 

Как мы видим, это честный (хотя и корявый) запрос на групповое удаление с условием.

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

using (var ctx = new ShopEntities()) {     var itemsUpdated = ctx.Users.Where(u => u.Orders.Count > 0).Update(u => new User { BonusCount = u.BonusCount + 1 });     //Осторожно, запрос уже улетел в БД      //и, несмотря на отсутствие вызова  ctx.SaveChanges(), данные были обновлены     Console.WriteLine("{0} users were updated", itemsUpdated); } 

Смотрим SQL запрос в профайлере.

UPDATE [dbo].[Users] SET  [BonusCount] = [BonusCount] + 1   FROM [dbo].[Users] AS j0 INNER JOIN ( SELECT      [Project1].[ID] AS [ID]     FROM ( SELECT          [Extent1].[ID] AS [ID],          (SELECT              COUNT(1) AS [A1]             FROM [dbo].[Orders] AS [Extent2]             WHERE [Extent1].[ID] = [Extent2].[UserID]) AS [C1]         FROM [dbo].[Users] AS [Extent1]     )  AS [Project1]     WHERE [Project1].[C1] > 0 ) AS j1 ON (j0.[ID] = j1.[ID]) go 

Это две основные функции из-за которых стоит установить данный пакет расширений.
Но есть и ещё сахар. Создатель пакета предлагает нам копить запросы на выборку, чтобы потом выполнять их за один подход. Для этого мы до материализации маркируем данные как Future() и потом, при материализации любого из объектов, остальные будут материализованы автоматически.

using (var ctx = new ShopEntities()) {     var alexUsers = ctx.Users.Where(u => u.Name == "Alex").Future();     var usersWithOrders = ctx.Users.Where(c => c.Orders.Any()).Future();      foreach (var item in alexUsers) //материализация всех данных была выполнена в этот момент за один round-trip к серверу.     {         Console.WriteLine("{0} {1}", item.ID, item.Name);     }      foreach (var item in usersWithOrders) //здесь нет выполнения SQL     {         Console.WriteLine("{0} {1}", item.ID, item.Name);     } } 

А вот такой был SQL запрос

 -- Query #1  SELECT      [Extent1].[ID] AS [ID],      [Extent1].[Name] AS [Name],      [Extent1].[IsTop10] AS [IsTop10],      [Extent1].[BonusCount] AS [BonusCount]     FROM [dbo].[Users] AS [Extent1]     WHERE (N'Alex' = [Extent1].[Name]) AND ([Extent1].[Name] IS NOT NULL);  -- Query #2  SELECT      [Extent1].[ID] AS [ID],      [Extent1].[Name] AS [Name],      [Extent1].[IsTop10] AS [IsTop10],      [Extent1].[BonusCount] AS [BonusCount]     FROM [dbo].[Users] AS [Extent1]     WHERE  EXISTS (SELECT          1 AS [C1]         FROM [dbo].[Orders] AS [Extent2]         WHERE [Extent1].[ID] = [Extent2].[UserID]     );  go 

Кроме расширения Future, так же доступны FutureCount, FutureFirstOrDefault, FutureValue.

Но и это ещё не всё. Представьте, что у вас есть модуль, который обрабатывает частые запросы к редко изменяемым данным. Например, авторизация пользователя. Хочется закешировать результаты? Пожалуйста. Как видно из кода, кэш не ограничивается контекстом, а остаётся актуальным даже после его повторного создания.

for (int i = 0; i < 2; i++) {     using (var ctx = new ShopEntities())     {         var alexUsers = ctx.Users.Where(u => u.Name == "Alex").FromCache();          foreach (var item in alexUsers) //i == 0 запрос в БД ушёл, i == 1 запроса в БД не было         {             Console.WriteLine("{0} {1}", item.ID, item.Name);         }     } }    

У метода FromCache есть перегрузка для точного указания времени кеширования.

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

ссылка на оригинал статьи http://habrahabr.ru/post/203214/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *