В первой своей попытке закрыть дыру в производительности Entity Framework'а я рассматривал только материализацию. Но дальше в процессе работы, как того и следовало ожидать, я наткнулся и на другое, более весомое ограничение. Операции вставки, модификации и удаления записей происходят тоже медленно. На 100 вставок EF посылает в базу 100 запросов на вставку, никак не пытаясь их сгруппировать.

Кроме этого, в одном из проектов была обнаружена одна неприятная ошибка: EF версии 5.0.0, при работе с Oracle, в Clob/Xml поля не позволяет вставлять строки более 2000 символов.

Для решения был создан компонент, который я назвал Context Items, со следующими возможностями:

1) Bulk Insert (MS Sql): в таблицы, не имеющие Identity в качестве первичного ключа возможно осуществить вставку методом Bulk Insert, который поддерживается базой данных Ms Sql Server. В случае с Identity нет способа надежно получить назад ключи, сгенерированные базой при вставке с помощью Bulk Insert, поэтому для таблиц имеющих такие ключи используется группировка нескольких обычных Insert-запросов в один запрос. Это работает существенно медленнее, чем Bulk Insert, но все же быстрее, чем через EF.

2) Sequenced Bulk Insert (MS Sql): альтернативой Identity обычно служит Guid, это решает проблему вставки, но создает другую проблему – в силу большей длины ключа операции Join начинают работать медленнее, кроме этого Guid непоследователен, и поэтому Clustered индексы не приносят своих преимуществ. Как решение данной проблемы начиная с MS Sql Server 2012 есть возможность использовать Sequence для создания первичных ключей. Это позволяет использовать целочисленные последовательные ключи, что позволяет использовать Clustered индексы, аналогично Identity, и одновременно позволяет использовать Bulk Insert для вставки. Компонент поддерживает только ацикличные Sequence с инкрементом 1.

3) Bulk Update (MS Sql): самой по себе в базе данных такой операции не существует, компонент воплощает ее последовательно выполняя следующие 4 операции:
   a) Создается временная таблица, имеющая тот же набор полей, что и целевая таблица
   b) Производится Bulk Insert данных во временную таблицу
   c) Выполняется Join-Update операция, которая переносит данные из записей временной таблицы в записи целевой таблицы, 
      имеющие совпадающие первичные ключи.
   d) Временная таблица удаляется

По причине того, что операция не атомарная, ее желательно исполнять в транзакции.

4) Bulk Delete (MS Sql): также как и Bulk Update, эта операция происходит в 4 шага:
   a) Cоздается временная таблица имеющая набор полей совпадающий с первичным ключом целевой таблицы
   b) Производится Bulk Insert во временную таблицу первичных ключей для записей, которые надо удалить
   c) Выполняется Join-Delete операция
   d) Временная таблица удаляется

5) Материализация: Функция перекочевала из предыдущего варианта, дополнительно я добавил в репозиторий тестовый проект, включающий в себя сравнение производительности материализации с micro-ORM Dapper. Context Items выполняет эту операцию примерно на 3-5% быстрее, чем Dapper, и на 40% быстрее, чем EF, при использовании AsNoTracking, без этого EF работает еще в несколько раз медленнее.

6) Array-bound Insert, Update and Delete (Oracle): Bulk Insert компонент, реализованный в ODP.Net не принимает во внимание ни триггеры ни constraint-ы, ни даже первичные ключи. Поэтому для вставки непосредственно в таблицу он не годится. Кроме того, он не поддерживает транзакции. Конечно можно было попытаться использовать временную таблицу как и в случае с MSSql, но я решил использовать метод, которые рекомендуется самим Oracle. Метод называется array-binding. Вкратце – запрос, посылаемый в базу, выглядит так, как будто мы вставляем одну запись, но в качестве параметров мы передаем не набор полей, а набор массивов полей, и таким образом вставляем, обновляем либо удаляем массив записей, а не одну запись. О методе можно почитать здесь

7) Equality members: Для всех сущностей генерируются методы GetHashCode и Equals, работающие с первичным ключом.

Компонент Context Items поддерживает EF версии 5.0.0 – 6.1.3, а также базы данных Oracle и MS Sql Server.
Поддерживается только database-first подход, имеется ограничение — имена сущностей должны совпадать с именами таблиц. Руки не дошли, чтобы это ограничение починить.

Компонент опубликован на nuget.org, с идентификаторами contextitemsmssql и contextitemsoracle. Подробнее об установке и использовании можно прочитать в репозитории на GitHub.com: github.com/repinvv/ContextItems

Комментарии (9)


  1. SVVer
    25.05.2015 21:25
    +3

    Хотелось бы понять, что заставляет Вас использовать EF, раз уже так много всего написано для ускорения? Это legacy code? Или есть еще какие-то объективные причины? Ведь в комментариях к первой статье уже предлагали linq2db, например.


    1. VladVR Автор
      25.05.2015 22:32
      +3

      Из всего этого, linq2db предлагает лишь одну операцию, что у меня под номером 1, и она с оговорками, операция считай такая же неюзабельная, как и OracleBulkCopy. Т.е. все эти операции придется реализовать для linq2db в той же мере, что и для EF. Второй, небольшой минус — цитата из readme «Now let's create a POCO class», EF предлагает всю схему сгенерировать с базы данных. Использование t4 templates неплохо бы улучшило эту разработку. А linq2entities провайдер там достаточно продвинут, я такого не ожидал.
      А так, основная причина использовать EF(как и например AngularJs) это заказчики и архитекторы. Т.к. их волнует больше поддерживаемость компонентов и коммюнити вокруг них. Эти два аспекта решают насколько легко будет вводить новых разработчиков в проект, и насколько вероятно будет наткнуться на проблему, для которой нельзя будет быстро найти решение в интернетах.


      1. VladVR Автор
        25.05.2015 23:33

        Поизучал еще немного, оказалось там есть набор t4 для генерации. Странно что информации об этом нет в readme на главной их странице, а есть лишь в другом репозитории, в тестовом проекте.


        1. Kefir
          26.05.2015 00:13
          +1

          При добавлении nuget пакета linq2db добавляются все T4-шаблоны, причем генерацию классов можно легко кастомизировать. Все инструкции лежат в ридми-файле, который ставится вместе с пакетом. Стоило все-таки попробовать.


  1. RouR
    26.05.2015 11:08

    На 100 вставок EF посылает в базу 100 запросов на вставку, никак не пытаясь их сгруппировать

    Install-Package EFUtilities

    public void InsertAsync(IEnumerable<T> entities, int batchSize = 1000)
    {...
      using (var db = (DbContext) Activator.CreateInstance(name))
      {
         EFBatchOperation.For(db, db.Set<T>()).InsertAll(entities);
         db.SaveChangesAsync();
      }
    

    github.com/MikaelEliasson/EntityFramework.Utilities


    1. chumakov-ilya
      26.05.2015 11:40
      +1

      Вот только EntityFramework.Utilities также использует SqlBulkCopy для множественной вставки:
      github.com/MikaelEliasson/EntityFramework.Utilities/blob/master/EntityFramework.Utilities/EntityFramework.Utilities/SqlQueryProvider.cs

      Генерацию INSERT-инструкций в EF, а также как самостоятельно использовать SqlBulkCopy, я подробно рассматривал тут, если интересно: habrahabr.ru/post/251397


    1. VladVR Автор
      26.05.2015 15:01

      Да, похожий аналог, дающий несколько меньше функций, чем есть у меня. В частности они никак не работают ни с Identity, ни c Sequence. По крайней мере не нашел. Сценарий достаточно распространен, если модели хоть сколь нибудь комплексные — вставить записи в одну таблицу, затем полученные ключи использовать для foreign-key полей при вставке во вторую таблицу.
      Я пару часов ломал голову, не мог понять почему там производительность BulkInsert операции выше, профилировал, в упор не видел. Оказалось, что они используют настройки по умолчанию, а у меня опции включают CheckConstraints, на таблице есть foreign-key constraint. Соответственно мой метод упадет если попытаться вставить невалидные данные, а их метод позволит вставить невалидные данные. Отключил для теста у себя опции — скорость стала 1 в 1.


      1. chumakov-ilya
        26.05.2015 15:53

        Почему поддерживается только Database-first?
        Развивать и держать на плаву планируете?

        Интерес у меня отнюдь не праздный — обдумываю, стоит ли публиковать собственный похожий проект.


        1. VladVR Автор
          26.05.2015 18:04

          В качестве источника используется edmx схема, которой для случая Code-first просто нет. Соответственно в сторону поддержки Code-first развивать не планирую.
          Опять же, все проекты, в которых я участвовал база данных была первостепенна. Соответственно компонент прошел испытания на них. Для code-first пришлось бы делать какой нибудь проект «для себя», иначе сколь нибудь реальных жизненных испытаний он бы был лишен.
          У меня сейчас более интересная задумка в работе в этом же направлении. Более высокоуровневая.