11 марта 2013 г.

Dapper + QueryObject, как замена ORM

Не так давно я утверждал, что нужно использовать высокоуровневую ORM и даже делал опрос на эту тему. Как показал опрос, не многие пишут SQL-запросы руками.

Проблематика

Для работы с типовыми проектами действительно не надо углубляться в вопросы специфики SQL. Но сейчас я веду проект, где без этого просто не обойтись. Речь про базы данных по 700ГБ и требования к отклику на UI менее 1 секунды. При этом происходит много разных расчетов, строятся графики и т.п.

Для начала я по привычке использовал NHibernate. Уже через пару итераций оказалось, что эта ORM мне не подойдет (не говоря уже про EntityFramework) по следующим причинам:

  • Скорость: создание сессии, маппинг и другие сопутствующие вещи отнимали до 200 мс. В моем случае это 20% времени, которое тратилось впустую;
  • Гибкость запросов: сначала я писал запросы на Linq, потом понял, что трудно сделать хитрые запросы и добавлять хинты через высокоуровневые интерфейсы типа Linq, QueryOver или Criteria. Из всех возможностей NHibernate я начал использовать только функцию ExecuteSql, но скорость маппинга была слишком низкой;
  • Утечки памяти: увы, но в NHibernate есть утечки памяти. Когда в вашей системе делается много запросов, это становится критичным. Сервисы через 20-30 часов работы падали с OutOfMemoryException и MemProfiler указал на NHibernate;
  • Много чтения, мало записи: в проекте мне надо считывать очень много данных, получается такой cRRRRud, поэтому UoW от NHibernate оказался тоже не нужен.

Dapper = Data Mapper

Раз уж я пишу SQL-запросы и выполняю их через ExecuteSQL, то было принято решение отказаться от ORM и уйти на более низкий уровень.

Выбор пал на библиотеку Dapper. Она представляет из себя несколько методов расширений для интерфейса IDbConnection.

Эта библиотека умеет из результатов запроса создавать объекты или просто выполнять SQL-скрипты, фактически, реализуя шаблон DataMapper. Запуск скриптов на SQL-сервере происходит как обычно через exec sp_executesql.

Самым популярным пользователем этой библиотеки является Stack Overflow, у нее почти 200 форков на GitHub и 42 тыс. скачиваний в NuGet. Всё это дает некоторую надежду на ее дальнейшее развитие и поддержку.

Ключевые особенности

В данный момент я использую Dapper в продакшене по нескольким причинам:

  1. Высокая скорость маппинга: различные сравнения можно посмотреть по ссылке Performance of SELECT mapping...;
  2. Низкоуровневое управление запросами: появилась возможность писать любые SQL-запросы, Dapper очень гибко делает маппинг, в том числе и в dynamic;
  3. Отсутствие накладных расходов: фактически мы управляем подключением к БД. Никаких маппингов, сгенерированных файлов, конфигурирования и т.п.

Примеры

Разработчики подготовили отличную демонстрацию возможностей: Tests.cs. Из примеров видно, на сколько легко и просто использовать эту библиотеку.

Шаблон QueryObject

Писать запросы, которые быстро выполняются, это хорошо, но приложение нужно развивать и поддерживать. Написание запросов на чистом SQL может привести к дублированию в коде и сложностям при рефакторинге.

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

После 2-х месяцев работы с Dapper я выделил решение, которое можно будет использовать в других проектах. В этом решении используется связка Dapper, шаблона QueryObject и MiniProfiler.

Исходный код на GitHub

Примеры использования

Библиотека в NuGet

Получается довольно компактный код:

// запуск профайлера можно делать опционально при старте приложения
MiniProfiler.Start();

using (IDbConnection dbConnection = ConnectionFactory.Create())
{
    var selectProduct = new SelectProduct();
    ProductDto productDto = dbConnection.Query<ProductDto>(selectProduct.ById(1))
                                        .SingleOrDefault();
}

Кэширование SQL на стороне сервера

Способ кэширования запросов SQL-сервером является еще одной причиной для инкапсуляции SQL-запросов. Дело в том, что кэшируется сам текст запроса. Это значит, что лишний пробел или изменение регистра будут влиять на выполнение запросов. Если мы сводим все запросы в одно место, то вероятность ошибки уменьшается.

Оценим решение

Как любое решение в разработке ПО, это имеет свои за и против.

Преимущества

  1. Высокая скорость работы
  2. Стабильность, отсутствие утечек памяти;
  3. Гибкость при создании запросов;
  4. Легок для работы приложения, не требователен к ресурсам;
  5. Чистый домен приложения, без дополнительных интерфейсов и изменения кода для работы ORM;
  6. Надо больше думать при работе с данными.

Недостатки

  1. Написание SQL-кода вручную;
  2. Нет готового кэша и готовых провайдеров для кэширования;
  3. Надо больше думать при работе с данными;
  4. SQL-код будет зависеть от СУБД.

Эти за и против я отмечал во время перехода с NHibernate на Dapper в своем проекте.

Расширения для Dapper

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

Аналоги

Dapper не единственная micro-ORM на .NET Framework, ниже список аналогов, которые могут больше подойти для вашего проекта:

Живая демонстрация

20 марта на очередной встрече INETA я буду делать доклад на тему Dapper+QueryObject и покажу несколько живых примеров. Ссылка на регистрацию: http://ineta.ru/MPPC/Meeting/2013-03-20-18-30. Данные для подключения трансляции появятся ближе к началу события.

30 комментариев:

  1. Спасибо. Очень полезно.

    ОтветитьУдалить
  2. Чистяков Андрей11 марта 2013 г., 14:57

    Александр, я правильно понимаю, что в данном случае Вы полностью отказались от ORM, т.е. связка Dapper+QueryObject применяется не только для select-запросов, но и для добавления/изменения/удаления записей?

    ОтветитьУдалить
  3. Да, всё верно. Сейчас в проекте есть только ссылки на Dapper. Как я уже говорил сохранять и изменять в проекте надо очень мало. Если бы часть про C(R)DU было большой, возможно я оставил бы NH

    ОтветитьУдалить
  4. Чистяков Андрей11 марта 2013 г., 15:18

    Да, я к этому и вел ) Я в своем проекте именно такое разделение и выбрал - для CDU - NHibernate, для запросов - просто ADO.NET. Спасибо за статью, буду ждать Ваш доклад на INETA.

    ОтветитьУдалить
  5. Виктор Глушенков11 марта 2013 г., 16:23

    Мы на одном из проектов использовали EmitMapper (http://emitmapper.codeplex.com/) и их библиотечку LightDataAccess. Сейчас на другом проекте используем Dapper.

    ОтветитьУдалить
  6. Евгений Румянцев11 марта 2013 г., 17:13

    Да, полезная инфа, спасибо.
    Кстати пока в примерах не увидел более прозрачного управления соединениями и транзакциями - а ля в spring-е, может что не дочитал...
    Ну или самому подружить сие с некоторым ioc из которого юзать коннекты?

    ОтветитьУдалить
  7. Сейчас я инжектирую IConnectionFactory, которая возвращает соединение.

    Реализация https://github.com/AlexanderByndyu/ByndyuSoft.Infrastructure/blob/master/source/Infrastructure.Dapper/ConnectionFactory.cs



    Что касается транзакций, то я думал, что из ConnectionFactory можно возвращать UnitOfWork, который будет с собой нести всю информацию о подключении, включая открытые транзакции, но пока это в проекте не пригодилось. Если вам понадобится, то жду PullRequest.

    ОтветитьУдалить
  8. Простите великодушно, мой коммент не по теме статьи.

    Вы пишете, что важным приоритетом для вас является скорость. Так почему же блог сделан таким тормозным? Банальное прокручивание люто-бешено тормозит.

    Да, я сейчас со старенького компа, ещё одноядерного. Но, чёрт возьми, я на нём Дум3 в своё время прошёл без тормозов. А тут банальная страничка блога рывками скроллируется.

    P.S. за статью спасибо.

    ОтветитьУдалить
  9. :)
    Блог делал не я, а платформа blogger.com

    ОтветитьУдалить
  10. Евгений Румянцев11 марта 2013 г., 19:51

    Может я чуть не прото :), к примеру как будет реализовываться декларативный способ работы с транзакциями - т.е. над методом ставим [Transaction] и всё что внутри метода обращается к dal делается в одной транзакции, сам dal фактически не управляет открытиями транзакций (как то так)

    ОтветитьУдалить
  11. Можете более подробно осветить проблему утечек памяти в nhibernate? Вы создали issue в трэкере?

    ОтветитьУдалить
  12. Размер базы в терабайт и основная нагрузка на чтение это симптомы для OLAP. Что вы думаете о возможности использовании OLAP для подобных проектов?

    ОтветитьУдалить
  13. Мы рассматривали такую возможность, но специфика проекта такова, что нашлось другое более гибкое решение. Вообще, в идеале, я настаиваю на каком-нибудь поисковом движке типа Sphinx'a, но пока спор с тех. лидом со стоны заказчика по этому вопросу затянулся.

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

    ОтветитьУдалить
  15. Как это обычно и делалось в ADO.NET: 1. Открываем соединение 2. Открываем транзакцию 3. При завершении метода с помощью объекта транзакции делаем Commit

    ОтветитьУдалить
  16. Виктор Глушенков19 марта 2013 г., 18:34

    В LightDataAccess если запрос не возвращает колонку, соответствующую свойству класса, его нужно явным образом включать в список игнорируемых. Это само по себе неудобно и некрасиво выглядит, но хуже то, что добавили свойство в класс → сломали все запросы.

    ОтветитьУдалить
  17. Добрый день, решил оставить небольшой отзыв по вышесказанному, не судите строго.

    1) В целом решение не выглядит законченным, так или иначе при работе с большим набором таблиц появится дубликация при работе со стандартными операциями (получения по id, удаление по id), что можно улучшить используя набор базовых классов. Dapper.Extensions - решает эту проблему.

    2) Возможно это специфика приложения, но оптимально было бы использовать кобинацию ORM -> CreateUpdateDelete а miniORM - Read, по скольку ORM в целом изабвляет разработчико от большого количества проблем в случае изменения схемы базы данных и упрощает написание кода, перейдя к чистым sql запросам мы вернулись по цепочки эволюции в начало

    3) С этим подходм необходимо не забывать про такие вещи как SQL-injection, разновидностей которых появилось очень много

    4) Ну и удачнее все-таки будет мапить не select а на хранимые процедуры, по скольку это также даст буст в производительносит на стороне сервера (возможно небольшой)

    ОтветитьУдалить
  18. 1) Решение только в стадии зарождения, использую Dapper около 2х месяцев.

    2) ORM можно использовать, но в нашем случае выигрыша не будет, 99% запросов - это выборки и схема меняется очень редко, думаю что вообще не поменяется :) Если вы были на вебинаре, то помните, что мы обсуждали возможные комбинации разных инструментов, конечно, ORM тоже входит в их список.

    3) При использовании параметризированных запросов проблемы нет

    4) К сожалению, запросы не очень сложные, но зависят от большого числа параметров и бизнес-логики, написать хранимую процедуру будет невозможно, либо это будут огромные процедуры с формированием динамического SQL внутри

    Спасибо за ваши рекомендации. Вы сами используете Dapper в продакшене?Dapper.Extensions?

    ОтветитьУдалить
  19. На текущем проекте использую dapper+dapper.extensions только для интеграционных тестов на взаимодействия с базой данных.

    В свое время несколько раз прыгали с NHibernate на Entity и обратно в надежде решить все проблемы и понял что всё - не решиться. Поэтому стремлюсь решать "конкретные" проблемы - например повышение скорости выборки запросов. По-скольку с развитием проекта и погружением в "инструмент" можно обнаружить дургие проблемы, тем более тут такой случай как sql в коде. Я знаю оно работает, но мы-то через это проходили =)

    ОтветитьУдалить
  20. Если проходили, то может подскажешь решение.

    Есть сложная логика формирования запроса, которая на выходе дает относительно несложный запрос (права на Linq или Criteria его не написать). Куда девать эту логику? Переносить в хранимки?

    ОтветитьУдалить
  21. С первого раза ответ не получился.

    Итак я сколняюсь к решение которое больше устроит комманду по следующим параметрам - удобство использования, простота отладки и свобода понимания и поддержки.

    - хранимые процедуры удобны тем что современные IDE для баз данных предоставляют интелисенс и проверку самого запроса на правильность. Тестировать их проблемно, с точки зрения понимая - такой же sql. Version Control для объектов базы данных - решаемо.

    - можно посмотреть в сторону каких-нибудь query builder аля http://www.codeproject.com/Articles/13419/SelectQueryBuilder-Building-complex-and-flexible-S, такой подход больше подходит программистам, плюс его можно как-никак покрыть юнит тестами, с точки зрения представления сложных запросов не знаю на сколько красивым получится решение


    - sql в коде - просто допустить ошибку, сложно ее обнаружить, большая строка в коде сложна для понимания


    По поводу Linq запросов, они иногда бывают намного хуже чем их sql эквиваленты, и дебажить эту рпелесть очень не весело.

    ОтветитьУдалить
  22. Рассматривался ли в качестве альтернативы BLToolkit? Если да, почему отказались?

    ОтветитьУдалить
  23. Нет, но уже несколько человек рекомендовали. Он у вас он в продакшене курится?

    ОтветитьУдалить
  24. Не выдержал, такого юмора и решил забросить пару камней. :)

    Использовал Dapper, в результате перешел на EF Code First. На мой взгляд это куда более удобное и не менее гибкое решение.

    "Высокая скорость работы" - Высокая на 500 тыс. записей? Кому вы такое количество собрались показывать?
    "Стабильность, отсутствие утечек памяти" - Как проверяли?
    "Гибкость при создании запросов" - Ребята, не надо клеить SQL из строк в коде приложения, да еще и параметры не использовать.
    "Легок для работы приложения, не требователен к ресурсам" - Согласен, dll легковеснее, но вот я сомниваюсь что это как-либо скажеться на работе приложения.
    "Чистый домен приложения, без дополнительных интерфейсов и изменения кода для работы ORM" - вот как раз здесь вам прийдется написать кучу прослоек, все же Linq куда более удобнее чем прослойки для склеивания SQL.
    "Надо больше думать при работе с данными" - А с другими ORM думать не надо?

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

    А вообще, я бы советовал при работе с высоконагруженными проектами, не экономить на хорошем DBA - это первоочередная и лучшая оптимизация по работе с БД!

    ОтветитьУдалить
  25. "Высокая на 500 тыс. записей? Кому вы такое количество собрались показывать?"

    Столько мы не показываем.

    "Как проверяли?" Профайлером. И NH и EF при сложных запросах, иногда начинают проливать память. Переход на Dapper это необходимость, я бы сам с удовольствием остался на NH.

    "Ребята, не надо клеить SQL из строк в коде приложения, да еще и параметры не использовать."

    Что вы предложение, если такие запросы не строятся на Linq и на Criteria?

    И мы используем параметры, Dapper это изначально поддерживал.

    "Согласен, dll легковеснее, но вот я сомниваюсь что это как-либо скажеться на работе приложения."

    Речь не про dll, а про кол-во доп. операций, которые требует ORM. Например, для управления жизненным циклом объектов и созданием DataContext.

    "вот как раз здесь вам прийдется написать кучу прослоек, все же Linq куда более удобнее чем прослойки для склеивания SQL."

    О каких прослойках идет речь? Посмотрите пример https://github.com/AlexanderByndyu/ByndyuSoft.Infrastructure/blob/master/source/Infrastructure.Dapper.Tests/CRUD/Select.cs

    Еще раз про Linq, невозможно на нем сложные запросы с хинтами писать, он такие запросы генерировать не умеет.

    " А с другими ORM думать не надо?"

    Тут ключевое слово БОЛЬШЕ. И при работе с EF думать надо, но тут нет высокоуровневых интерфейсов.

    "Как показывает практика, тормоза происходят на уровне БД, а не в маппинге результатов"

    У нас есть 2 проекта, в которых маппинг EF оказался ну очень долгим, особенно это касалось вложенных сущностей. Видимо для вас 100 мс это незаметное время, для нас 100мс это очень долго.

    "А вообще, я бы советовал при работе с высоконагруженными проектами, не экономить на хорошем DBA - это первоочередная и лучшая оптимизация по работе с БД!"

    Такой человек у нас тоже есть.

    Сергей, если вы думаете, что мы взяли первый попавшийся инструмент, то это не так. У меня 4 года опыта с EF и NH, причем плотной работы и глубокого изучения. Dapper стал необходимостью, причины я описал.

    ОтветитьУдалить
  26. Извиняюсь за поздний ответ.
    В крупных проектах не используем, только в небольших.

    ОтветитьУдалить
  27. А в крупных что используете?

    ОтветитьУдалить