Разработка софта всегда была поиском баланса между разными аспектами, вроде скорости разработки (как быстро ты выкатываешь новые фичи), производительности приложения, потребления им памяти, красотой интерфейса и отполированностью логики.

Почему важна скорость разработки

В большинстве случаев скорость разработки важнее, чем производительность и потребление памяти. Производительность бывает достаточной, потребление памяти бывает приемлемым для выполнения задач приложением, а вот от скорости разработки напрямую зависит прибыльность бизнеса, поэтому скорости разработки никогда не бывает мало. От неё зависит, сколько бизнесу придётся потратить на этап разработки приложения, сколько времени придётся проводить под давлением рисков. Возможно, это приложение для рынка и конкуренты могут успеть раньше, тогда ничего критичнее сроков просто нет.

Конечно, приложение, которое еле двигается и которым невозможно пользоваться не выиграет конкурентную борьбу. Точно также как и приложение, требующее слишком много памяти для своей работы. Но если идёт речь об ускорении приложения на 20% или экономии пары-тройки месяцев разработки, то выбор очевиден. Эти 20% ускорения может никто и не заметит, а вот дыру в бюджете не заметить очень сложно.

Как ORM делает разработку быстрее

ORM - это как раз инструмент, экономящий время разработки. Но за счёт чего?

В первую очередь, ORM делает код короче. Он прячет "под капотом" не интересные нико��у тонны кода, переносящего значения из колонок таблицы в поля класса и назад. Это ещё даёт типобезопасность. Тип поля в классе всегда будет соответствовать типу данных колонки.

Конечно, типобезопасность касается только статически типизированных языков, но подсказки и автодополнение от IDE работают для всех. С автодополнением работать не только приятнее, но и ошибок меньше. И тоже быстрее.

Так как кода просто меньше, он быстрее пишется и он быстрее читается. Второе важнее для больших проектов, где ограничением является контекстное окно биологической нейросети. Разбивка кода по уровням и функционально ограниченным функциям - это отдельный вид искусства, но скрытие технических подробностей - отличный инструмент для мастеров такого искусства.

ORM позволяет работать на более высоком уровне абстракции. Он берёт на себя работу с полями и выдаёт наружу объекты. Можно взять общую часть запроса и модифицировать её для нескольких частных случаев, например используя полиморфизм. Можно добавлять условия в запрос в зависимости от того, что хочет пользователь вашего приложения. С raw SQL такое сделать гораздо сложнее.

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

ORM знач��тельно упрощает рефакторинг данных. Это очень недооценённая возможность маппера. Вам будет гораздо проще приводить в порядок схему данных с ORM, чем без него. Очень сложно вывести конкретные метрики, насколько рефакторинг помогает в дальнейшей разработке, но это может дать ускорение процесса разработки в десятки раз, особенно на длинной дистанции.

Да, новый слой абстракции добавляет сложности к кривой вхождения в проект. И если у вас маленький проект с десятком таблиц, которые почти не меняются, то вам, скорее всего, ORM не нужен. Он нужен для управления высокой сложностью проекта, а в малых проектах этой сложности просто нет и ORM даст только прирост сложности за счёт нового слоя абстракции.

Влияние на производительность

В начале статьи я говорил, что разработка - это поиск компромиссов. Означает ли, что внедряя ORM мы потеряем в быстродействии и потреблении памяти? Вовсе нет. Давайте рассмотрим ситуацию комплексно.

Конечно, ORM добавляет какой-то оверхед на составление запросов и маппинг данных. Но давайте честно, в сравнении с временем I/O операций базы данных этот оверхед совершенно незначителен. А значительно то, что за счёт меньшего объёма кода и более лёгкой алгоритмической оптимизации работа с ORM может быть в разы быстрее работы с чистым SQL. Да, SQL тоже можно оптимизировать. Только сложнее увидеть всю картину целиком, да и менять страшно. Может же что-нибудь сломаться. Поэтому стараются лишний раз не трогать и не оптимизировать. И получается парадоксальная ситуация - SQL работает чуть быстрее, как только он написан, но намного медленнее после множества итераций разработки.

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

Потребление памяти

С потреблением памяти тоже не всё так просто.

Идеология реляционной базы данных подразумевает, что ответ на запрос будет плоской таблицей. Допустим, мы возьмём большую табличку, где 100+ колонок (например orders) и сделаем выборку один-ко-многим с маленькой таблицей на 5 полей (например positions). Если мы будем хранить ответ "как есть", то на каждую позицию мы будем хранить копию всех 100+ полей из первой таблицы. Здесь перерасход памяти может быть и десяти- и стократным. Хороший ORM упакует записи из второй таблицы в единственный экземпляр из левой таблицы.

Без заранее известной информации о типах данных вашему приложению придётся хранить данные в обобщённом виде, указывая тип для каждого значения. Для разработчиков на динамически типизированных языках это в порядке вещей, но в статически типизированных это называется "боксинг" и приводит к перерасходу памяти. Int32, будучи в подобной структуре, занимает 8 байт на стеке, плюс 24 байта в куче (x64 clr). ORM раскладывает данные по типизированным полям, сокращая в разы потребление памяти.

Entity Framework

EF стоит в ряду ORM особняком и далеко не все его возможности доступны в других ORM. Я вроде бы это понимаю, но я пишу эту статью, как прожжёный дотнетчик, представляя себе именно его возможности, где есть грациозное решение N+1, есть LINQ и много чего другого.

Коварный Entity Framework, да впрочем и я тоже
Коварный Entity Framework, да впрочем и я тоже

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

Синхронизация структуры

Самая первая задача любого типа ORM - обеспечить синхронность структуры базы данных и структуры объектов приложения. В соответствии с этой структурой колонки таблицы загружаются в поля и потом обратно. Собственно, это и есть маппинг. Для того, чтобы это корректно работало, где-то нужно брать "оригинал" структуры на этапе написания приложения. Это может быть база данных (DB-first), код (code-first) или отдельная модель (model-first). Code-first проецирует структуру из кода в базу данных. Это удобно, когда есть несколько окружений, но ломается, когда есть несколько независимых сервисов, где каждый пытается подстроить БД "под себя". Его синхронизация сложная, потому что требует загрузки существующей структуры базы данных, корректного сравнения моделей и сложного механизма генерации DDL. Этот подход выходит за рамки статьи, поэтому я его рассматривать не буду.

DB-first и model-first используют кодогенерацию сущностей для синхронизации структуры. Это значительно проще, нужно всего лишь загрузить структуру из базы данных (или взять файл модели в случае model-first) и создать заново файлы сущностей в проекте.

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

Эту негибкость ORM часто ругают и справедливо. Я эту проблему решил с помощью генераторов с открытым исходным кодом. Вы можете взять готовый генератор с лицензией MIT и модифицировать его под особенности своего проекта. Это проще, чем писать свой генератор с нуля, потому что структуру данных подготавливает для генератора OrmFactory. Этот инструмент работает напрямую с базой данных и умеет сохранять структуру в файл проекта. Его философия в том, что он предоставляет интерфейс для проектирования схемы данных с возможностью синхронизации окружений с кодовой базой, оставляя ORM только ту работу, с которой он справляется лучше всего - маппинг.

Ещё одно неприятное следствие синхронизации схемы - это отсутствие возможности поменять имя поля в сгенерированном коде. Точнее, поменять-то можно, но оно опять вернётся к первозданному виду при следующей генерации. Это может быть вопросом вкуса, а может быть необходимостью для рефакторинга, когда имена в БД трогать нельзя, но надо назвать как-то человечнее в коде, а может быть суровой необходимостью. Например, в C# имя поля не может совпадать с именем класса. А в БД имя колонки с именем таблицы - запросто. И вот что делать?

Я уже не говорю про такую простую вещь, как приведение полей из snake_case в PascalCase, который чаще всего используется в проектах. Не все ОРМ позволяют автоматически конвертировать кейс-стайл имён.

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

Спасибо, что дочитали. Всем Пше Згыр!

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