Сергей Тарасов - Дефрагментация мозга. Софтостроение изнутри
Как только разработчики реализовали CRUD-логику, начинается основное действо. Использовать сиквел напрямую теперь затруднительно. Если не касаться стратегий отображения и проблем переносимости приложения между СУБД, по сути каждый SQL-запрос с соединениями, поднявшись в домен, сопровождается специфической проекцией табличного результата на созданный по этому случаю класс. По этой причине приходится использовать собственный язык запросов ORM. Нестандартный, без средств отладки и профилирования. Если он, язык, вообще имеется в данном ORM. Для поддерживающих соответствующую интеграцию среда. NET предоставляет возможность использовать LINQ[57], позволяющий отловить некоторые ошибки на стадии компиляции.
Сравните выразительность языка на простом примере, который я оставлю без комментариев:
SQL
SELECT *
FROM task_queue
WHERE
id_task IN (2, 3, 15)
AND id_task_origin = 10
NHibernate HQL
IList<TaskQueue> queues = session
CreateQuery("from TaskQueue where Task.Id in (2, 3, 15) and TaskOrigin.Id = 10")
List<TaskQueue>();
NHibernate без HQL с критериями
IList<TaskQueue> queues = session.CreateCriteria()
Add(Expression.In("Task.Id", someTasks.ToArray()))
Add(Expression.Eq("TaskOrigin.Id", 10))
List<TaskQueue>();
LINQ (NHibernate)
IList<TaskQueue> queues = session
Query<TaskQueue>()
Where(q => someTasks.Contains(q.Task.Id) &&
q. TaskOrigin.Id == 10).ToList();
Внезапно оказывается, что собственный язык запросов генерирует далеко не самый оптимальный SQL. Когда БД относительно небольшая, сотня тысяч записей в наиболее длинных таблицах, а запросы не слишком сложны, то даже неоптимальный сиквел во многих случаях не вызовет явных проблем. Пользователь немного подождёт.
Однако запросы типа «выбрать сотрудников, зарплата которых в течение последнего года не превышала среднюю за предыдущий год» уже вызывают проблемы на уровне встроенного языка. Тогда разработчики идут единственно возможным путём: выбираем коллекцию объектов и в циклах фильтруем и обсчитываем, вызывая методы связанных объектов. Или используем тот же LINQ над выбранным массивом. Количество промежуточных коротких SQL-запросов к СУБД при такой обработке коллекций может исчисляться десятками тысяч.
Триггер как идеальная концепция для NHibernate
Обычно разработчикам баз данных я рекомендую избегать необоснованного использования триггеров. Потому что их сложнее программировать и отлаживать. Оставаясь скрытыми в потоке управления, они напрямую влияют на производительность и могут давать неожиданные побочные эффекты. Пользуйтесь декларативной ссылочной целостностью (DRI[58]) и хранимыми процедурами, пока возможно. А если ваш администратор баз данных склонен к параноидальным практикам «запрещено всё, что не разрешено», избегайте программировать на уровне СУБД, исключая критичные по производительности участки. Приходится говорить это с сожалением…
Однако стоит посмотреть на слой домена, живущего под управлением NHibernate, как становится ясно, что триггер в СУБД – это достаточно простая и хорошо документированная технология. В то время как NHibernate предлагает прикладному разработчику целый зоопарк триггероподобных решений.
Во-первых, имеется древний способ реализации классом домена интерфейса из пространства имён NHibernate.Classic. Например, IValidate. Вроде бы удобно: реализовал и делай проверки, генерируя исключения для отмены транзакции. Но вот незадача: при удалении объекта этот метод не срабатывает, нужно использовать другие подходы.
Во-вторых, после осознания авторами недостаточности IValidate и ILifeCycle была введена система прерываний (interceptors). Это уже больше, чем бесплатный хлеб на завтрак. Однако в обработчиках типа Save или FlushDirty в качестве аргументов используются массивы состояний объекта. То есть изменять сам объект в них напрямую нельзя: в общем случае это просто не срабатывает, но могут быть и побочные эффекты. Нужно, ни много ни мало, поискать индекс элемента в массиве имён свойств объекта, затем по найденному номеру изменить значение в другом массиве текущего состояния объекта. Что-то вроде такого кода:
Изменение свойств объекта в обработчике NHibernate
int index = Array.IndexOf(propertyNames, "PhoneNumber");
if (index!= -1)
state[index] = "(123)456789";
Создавать новые, извлекать, изменять или удалять существующие объекты с последующим сохранением внутри обработчика совершенно не рекомендуется. Кроме собственно гонок (race condition), когда обработчик одного класса создаёт другой, а тот что-то делает с первым, могут быть и другие эффекты, включая бесконечную рекурсию. Шаг вправо, шаг влево – стреляют боевыми и без предупреждения. Неплохая иллюстрация к теме декларируемой безопасности языка C# или Java. Рекомендуемая практика обхода ловушек такого рода – запрограммировать собственную защищённую (thread safe) очередь, куда складывать все созданные или изменённые объекты, а в событиях BeforeCommit или AfterCommit эту очередь обрабатывать.
В-третьих, механизм прерываний также признан несовершенным, после чего был введён механизм событий (events), коим, начиная со второй версии, всем следует пользоваться.
Дело в том, что в сессии вы можете зарегистрировать только один класс, реализующий обработчики прерываний. И если у вас достаточно много разной обработки, то получается так называемый «волшебный класс», который реализует всё. Это неудобно, даже если использовать класс в качестве пустого фасада.
Теперь же вы можете зарегистрировать неограниченное количество классов-слушателей (listeners), ожидающих то или иное событие и соответствующим образом реагирующих на него. Один класс может реализовывать несколько обработчиков событий, будучи зарегистрированным для прослушивания нескольких их типов.
С точки зрения архитектуры это несомненный плюс, реализацию можно разбить на независимые классы. Однако для снижения побочных эффектов, прежде всего рекурсии и гонок, не сделано ничего. В том же SQL Server, напомню, рекурсия в триггерах отключена по умолчанию, поскольку трудно сходу придумать случай, когда она нужна. А в событиях NHibernate каждый сам себе вредитель. При этом однозначной методики и документации нет, нескольким десяткам типов событий в официальной документации отведено меньше страницы. Существует огромное количество записей в блогах, тиражирующих одни и те же конкретные примеры – аудит, прежде всего. Но однозначной выверенной практики нет, в каждом конкретном случае надо проводить тесты. Например, для манипуляции объектами рекомендуется создавать дочернюю сессию, что также не всегда избавляет от побочных эффектов.
В итоге имеем плохо документированную нестабильную систему, которая при отладке в разы труднее столь нелюбимых разработчиками триггеров баз данных. Тем не менее, если разработчик пишет код слоя домена, альтернатив практически нет. Генерация скелета кода по модели облегчает работу, но риски возникновения ловушек многопоточной обработки по-прежнему остаются на совести рядового программиста. А создать такие ситуации несложно. Поэтому надо признать, что использование обработчиков событий слоя домена должно быть рекомендовано еще в меньшей степени, чем использование триггеров слоя хранения данных.
ORM на софтостроительной площадке
На короткое время судьба забросила меня в качестве консультанта в лоно одной софтостроительной фирмы, разработавшей и поддерживающей специализированную систему документооборота для управления жизненным циклом товаров. Система относительно небольшая по функционалу, а вот клиенты хоть и мало-
численные, но крупные, то есть способные упорно настаивать на своих требованиях, иногда противоречивых, аргументируя их соответствующим бюджетом.
В процессах взаимодействия фирм весьма отчётливо действуют физические законы всемирного тяготения. Небольшой планете-фирме, чтобы не упасть на большую, разбившись вдребезги, необходимо развить как минимум первую космическую скорость. В этом случае она будет стабильно вращаться вокруг большой в качестве спутника. Чтобы оторваться от поля тяготения большой планеты и начать самостоятельный полет требуется уже вторая космическая скорость.
В течение последних месяцев в фирме происходила попытка выйти на вторую космическую. Поскольку процесс, обеспечивающий первую космическую, был близок к тому, что называют экстремальным программированием, было принято решение продолжать в том же духе, назвав все это звонким словечком «скрам»[59].
В принципе, основные элементы процесса имелись в наличии. Коллективное владение кодом, также известное, как личная безответственность при его написании, утренние «пионерские линейки» вместо чётких спецификаций, практически полное отсутствие документации, частые, до одного раза в 1–2 недели, релизы и связанный с этим нескончаемый аврал, работа в тесном и жарком помещении общего зала. Последнее вызвано объективными причинами: для поддержания жизнедеятельности муравейника требуется всё больше работяг.