Современные информационные технологии поражают своей мощью, ошеломляют открывающимися возможностями, обескураживают заложенным в них техническим совершенством, но есть один смехотворный пункт, об который IT раз за разом снова и снова ломает зубы. Показать пользователю данные из базы, получить от него ввод, положить обратно в базу, показать результат. Поля ввода, кнопочки, галочки, надписи — казалось бы, что в них может быть такого запредельно сложного, чтобы потребовалось городить головоломные конструкции типа фреймворков поверх шаблонизаторов поверх фреймворков поверх транспайлеров? И почему несмотря на все колоссальные усилия мы имеем то, что игрушечные примеры по туториалу, конечно, делаются легко и приятно, но как только инструментарий сталкивается с реальными задачами реальной жизни… как бы это сказать помягче… с ростом сложности решаемых задач наблюдается сильная нелинейность возрастания сложности реализации. Ладно бы речь шла о чём-то действительно головоломном уровня теоретической физики или космической техники, так ведь нет же — кнопочки и галочки. Почему эта ерунда десятилетиями продолжает отравлять жизнь гражданам и трудовым коллективам?
Причин, наверно, как всегда оно бывает, много. Наверно все они так или иначе достойны рассмотрения, но здесь и сейчас мы поговорим о задаче объектно-реляционного отображения (Object-Relational Mapping, ORM), которая всегда в каком-либо виде стоит за всеми этими «кнопочками и галочками».
Что мы знаем про ORM
- Хранение данных в реляционных таблицах — не сказать, что совсем простое дело, но в целом и идея, и пути её применения достаточно понятны и хорошо исследованы.
- С объектным программированием не всё так хорошо (есть несколько конкурирующих подходов), но в целом с реализацией и применением технологий тоже всё более-менее понятно.
- И то, и другое — про данные, их хранение и обработку. То есть, по сути, про одно и то же.
- Нам кажется логичным, что должен существовать простой, понятный, удобный, предсказуемый и универсальный мостик между этими двумя мирами.
- И мы каждый раз этот мостик легко находим, но вот незадача, его простота, понятность, удобство, предсказуемость и универсальность не работают за пределами простых примеров из туториалов.
- Страдают все: и разработчики, которым приходится делать массу чрезвычайно скучной работы, и пользователи, которым приходится сражаться с корявым софтом, и бизнес, реализация потребностей которого внезапно оказывается невыносимо долгой и дорогой, и индустрия в целом.
- Я видел много разных ORM, но не видел ни одного хорошего. То есть такого, который за пределами простых примеров не превращается в обузу и прокрустово ложе.
Почему всё так причудливо
Идейная основа теории и практики реляционных баз данных — исчисление предикатов, то есть раздел математики. Что касается ООП, то аналогичная по прочности идейная основа там отсутствует. Базовую идею ООП можно попытаться сформулировать примерно так: поскольку мир состоит из объектов, то его, этот мир, было бы удобно моделировать созданием объектов внутри программной системы. В этом понимании сразу две ошибки. Во-первых, мир сам по себе не состоит и никогда не состоял из объектов. Во-вторых, извините, но зачем программа обязательно должна моделировать мир? То есть мы имеем концептуально неверное утверждение, прекрасно дополняемое бессмысленной постановкой задачи.
Всякий ORM – попытка чётко протянуть унифицированное соответствие между, по сути, разделом математики и рыхлым набором разнообразных практик, основанном на соображениях удобства, исторически сложившихся подходах, а также зачастую на легендах, мнениях авторитетов, да и просто заблуждениях. In vitro это можно заставить работать, но in vivo оно обречено выглядеть жалко и приносить больше горя, чем радости.
О неизбежности объектной ориентированности
Тем не менее, необходимость объектной ориентированности нашего софта — наша неизбежная реальность. Неизбежность эта основана в первую очередь на том, что оперирование объектами составляет суть и основу любой нашей деятельности. Мир сам по себе не состоит из объектов, но для того, чтобы что-то в этом мире понять и что-то с этим миром сделать, мы сами для себя объявляем его части объектами, называем их именами, пытаемся понять их поведение, прикладываем к ним усилия для получения желаемых результатов. Это наш способ функционирования, и уйти от него невозможно, да и не нужно. Всё есть объект не потому, что так оно и есть на самом деле, а потому, что мы по-другому не можем. То, что никоим образом не может являться объектом, полностью лежит за пределами нашей способности осмысления и не может служить предметом приложения наших усилий.
Даже если программа написана без использования техник ООП, в ней неизбежно присутствуют объекты (в широком смысле этого слова), манипулируя которыми разработчик решает свою задачу — переменные, типы данных, операторы, алгоритмы, синтаксические конструкции, функции, модули. С точки зрения пользователя программа тоже есть набор объектов, с которыми он взаимодействует — кнопки, надписи, поля ввода, страницы, сайты, да и вся система целиком.
Что мы храним в своих базах данных
Как говорилось выше, реляционные базы данных основываются на исчислении предикатов. Предикат — это сформулированный и в нашем случае сохранённый на носитель факт. На всякий случай замечу, что реляционность базы данных — это не только и не столько про связи между таблицами по внешним ключам. В правильной терминологии отношениями (relations) называется то, что мы по-простому называем таблицами. То есть даже если в вашей базе всего одна таблица с двумя колонками (например, имя и номер телефона), у вас уже реляционная база, устанавливающая отношение между двумя множествами, в данном случае множествами имён и номеров телефонов. Реляционная база не хранит объекты, она хранит факты. Хранимые факты, конечно же, имеют предмет («о чём этот факт?»), и когда мы пытаемся научить систему отвечать на этот вопрос, у нас появляются сущности, то есть объекты, с которыми связаны факты. Если работать правильно, структура базы у нас рождается из серии ответов на вопрос «какого рода факты мы намерены сохранять?», и лишь на следующем этапе у нас появляется нечто, напоминающее объекты, придающие фактам предметность. Можно, конечно, проектировать и «от объектов», но я бы не рекомендовал так делать иначе как на лабораторных работах по предметам, не связанным напрямую с проектированием баз данных. Слишком велика опасность тяжёлых архитектурных просчётов. Кроме того, это как минимум неудобно.
Небольшое лирическое отступление про объектные базы данных. Очень простая мысль: если мы устали от проблем с ORM, то, может быть, нам сделать что-нибудь с той его частью, которая «R»? Пусть наша база данных будет не жёстким и требовательным реляционным чудовищем, а чем-нибудь модным молодёжным, специально заточенным на хранение объектов. Какая-нибудь бессхемная NoSQL-база, например. Но в конечном итоге внезапно оказывается, что NoSQL-ные ORM ещё кривее и противоестественнее, чем старые добрые SQL-ные. Да и вообще, иметь и с удовольствием эксплуатировать бессхемную СУБД мы можем, но бессхемных данных в природе не бывает. Нет ничего более беспомощного, безответственного и безнравственного, чем ORM для бессхемных баз данных.
Хороший ORM
Хороший ORM – отсутствующий ORM. Ну серьёзно. Посмотрите на любую из своих использующих ORM систем и честно попытайтесь ответить себе на вопрос: какие профиты приносит этот монстр? Чем обусловлено его применение кроме как несбывшимися обещаниями счастья и многократно дискредитировавшими себя предрассудками? Безусловно, найдутся некоторые полезные удобняшки, но что они на фоне привносимых архитектурных деформаций и постоянно возникающих на ровном месте проблем с производительностью?
Как правило, «низкоуровневый» API базы данных прост, удобен, полн и консистентен. Обычно хватает пальцев рук для того, чтобы перечислить все функции. Выучить их — не бог весть какая задача.
Попробую набросать набор архитектурных принципов, позволяющих замэппить объекты на базу данных без использования ORM:
- Храним факты, оперируем объектами. Помним, что база данных хранит факты, а объектные модели, задействованные в обработке данных — это проекции наборов фактов с разных точек зрения. Например, для приведённого примера с именами и номерами телефонов у нас может быть класс Abonent, для которого может быть сохранено несколько номеров, а также класс PhoneNumber, для которого может быть задано несколько абонентов (не забываем, что кроме персональных мобильных номеров у нас кое-где ещё остались квартирные и офисные телефоны). А таблица в базе — всего одна, задающая отношение многие-ко-многим между множеством имён и номеров телефонов. Просто две разные проекции. Этот подход, кстати, нормально масштабируется на гораздо более сложные случаи, позволяя вам иметь в системе такие полезные классы, как, например, «средний объём продаж за заданный период по заданной комбинации критериев».
- Факты проецируются в объекты и обратно через проблемно-ориентированный API. Без применения решения, претендующего на универсальность. Если вдруг ещё не умеете сочинять удобные API, так научитесь. И что тоже немаловажно, приучите себя сразу же их документировать.
- Порядок превыше всего. Если вы используете классический вариант СУБД с жёсткой схемой данных, то эта схема уже сама по себе вносит порядок в работу с данными. Дополнительная структура, закодированная структурой объектов, просто избыточна. Если вы используете бессхемную СУБД, то вам, конечно, придётся приложить некоторые усилия к тому, чтобы ваша база данных не превратилась в гору мусора вследствие того, что разные разработчики имеют разное представление о том, что где хранится.
- СУБД-независимость (если нужно). Если наиболее интересным свойством ORM для вас является СУБД-независимость, то используйте такие спецсредства, как ODBC, JDBC, ADO, или если это невозможно, изготовьте собственный уровень абстракции. Это не так сложно, как сначала может показаться. Вам же для вашей задачи не нужна поддержка абсолютно всех возможностей абсолютно всех СУБД, правда ведь?
- Не забываем про дополнительные аспекты работы с данными. Такие, как, например, разделение доступа к данным (которое может быть сколь угодно сложным), мониторинг, репликации и прочее. Но здесь у меня для вас хорошие новости: поскольку в вашем любимом ORM нужный вам доп. аспект всё равно реализован по принципу «всем не угодишь, ешьте что дают», отказ от сомнительного сервиса, с которым приходится больше бороться, чем сотрудничать, в конечном счёте окажется стратегически правильным решением.
Итого
ORM – очень больная для нашей индустрии тема. В эпоху, когда облачный искусственный интеллект бороздит просторы квантового блокчейна, подавляющее большинство трудовых ресурсов занято прикручиванием бизнес-логики и пользовательского интерфейса к базам данных. Миллионы строк ужасного кода, забивание гвоздей микроскопами, боль и отчаяние повсеместно сопровождают этот творческий процесс. Один из корней этого печального положения дел — чрезвычайно стойкое заблуждение, что универсальный ORM в принципе возможен. А он невозможен, и этому есть фундаментальная причина, не подлежащая устранению. Осознание этого факта — первый шаг по направлению к выходу из этого кошмара. Выход — возможен, альтернативы — есть, но сначала — осознать, прочувствовать и научиться держать в фокусе внимания.
P.S. Я искренне извиняюсь перед теми братьями по профессии, кто вложил массу времени и усилий в создание многочисленных универсальных лучших в мире ORM. Мне правда очень жаль.