Шрифт:
Такая ситуация приводит разработчиков ORM к необходимости создавать собственный SQL-подобный язык для манипуляции объектами и уже его транслировать в сиквел [51] (см. HQL [52] ). Или использовать непосредственно SQL с динамическим преобразованием результата в коллекцию объектов.
В противном случае прикладной программист обречён на извлечение из БД и последующую обработку больших массивов данных непосредственно в своём приложении. Примерно так же обрабатывали табличные данные при отсутствии встроенного SQL разработчики на ранних версиях Clipper в конце 80-х годов. Там это называлось «навигационная обработка». Думаю, термин уместен и здесь.
В эпоху массового перехода с Clipper-подобных программ и файл-серверных технологий на клиент-серверные РСУБД многие приложения и их разработчики продолжали использовать навигационный подход. Приложения работали медленно, зачастую блокируя работу в многопользовательской среде. Потому что для эффективной работы с РСУБД нужно использовать подходы, ориентированные на обработку множеств на сервере, предполагающие наличие у разработчика умений работать с декларативными языками.
Тем не менее, получив в распоряжение ORM, программист зачастую возвращается к навигационным подходам обработки массивов данных вне РСУБД лишь с той разницей, что теперь этот массив, как хочется надеяться, является содержимым целой таблицы.
Почему? Недостаток знаний РСУБД пытаются возместить дополнительными уровнями абстракций. На деле же выходит обратное: уровни абстракции скрывают не детали слоя хранения объектов от программиста, а, наоборот, его некомпетентность в области баз данных от СУБД. До некоторого времени.
Несмотря на толстый слой абстракций, предоставляемый ORM типа Hibernate, заставить приложение эффективно работать с РСУБД без знаний соответствующих принципов ортогонального мира и языка SQL практически невозможно.
Но попытки продолжаются. Одни по-прежнему разрабатывают проекторы для своих внутренних нужд, зачастую очень лёгкие. Другие ищут упрощение и выход в noSQL [53] . Но в выигрыше пока остаются программисты и консультанты, обладающие «базоданными» компетенциями и зарабатывающие на тех, кто ими не обладает.
Как обычно используют ORM
На софтостроительных презентациях часто рисуют красивые схемы по разделению слоёв представления, бизнес-логики и хранимых данных. Голубая мечта начинающего программиста – использовать только одну среду и язык для разработки всех слоёв и забыть про необходимость знаний реляционных СУБД, сведя их назначение к некоей «интеллектуальной файловой системе». Аббревиатура SQL вызывает негативные ассоциации, связанные с чем-то древним, не говоря уже про триггеры или хранимые процедуры. На горизонте появляются добрые люди, с книгами признанных гуру о домен-ориентированной разработке [54] под мышкой, заявляющие новичкам примерно следующее: «Ребята, реляционные СУБД – пережиток затянувшейся эпохи 30-летней давности. Сейчас всё строится на ООП. И есть чудесная штука – ORM. Начните использовать её и забудьте про тяжёлое наследие прошлого!»
Ребята принимают предложение. Дальше эволюция разработки системы примерно следующая:
• Вначале происходит выбор ORM-фреймворка для отображения. Уже на этом этапе выясняется, что с теорией и стандартами дело обстоит плохо. Впору насторожиться бы, но презентация, показывающая, как за 10 минут создать основу приложения типа записной книжки контактов, очаровывает. Решено!
• Начинаем реализовывать модель предметной области. Добавляем классы, свойства, связи. Генерируем структуру базы данных или подключаемся к существующей. Строим интерфейс управления объектами типа CRUD [55] . Все достаточно просто и на первый взгляд кажется вполне сравнимым с манипуляциями над DataSet [56] – тем, кто о них знает, конечно – ведь не все подозревают о существовании табличных форм жизни данных в приложении за пределами сеток отображения DBGrid.
Как только разработчики реализовали CRUD-логику, начинается основное действо. Использовать сиквел напрямую теперь затруднительно. Если не касаться стратегий отображения и проблем переносимости приложения между СУБД, по сути каждый SQL-запрос с соединениями, поднявшись в домен, сопровождается специфической проекцией табличного результата на созданный по этому случаю класс. По этой причине приходится использовать собственный язык запросов ORM. Нестандартный, без средств отладки и профилирования. Если он, язык, вообще имеется в данном ORM. Для поддерживающих соответствующую интеграцию среда. NET предоставляет возможность использовать LINQ [57] , позволяющий отловить некоторые ошибки на стадии компиляции. Сравните выразительность языка на простом примере, который я оставлю без комментариев:
SQL
SELECT *
FROM task_queue
WHERE
id_task IN (2, 3, 15)
AND id_task_origin = 10NHibernate 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 в качестве аргументов используются массивы состояний объекта. То есть изменять сам объект в них напрямую нельзя: в общем случае это просто не срабатывает, но могут быть и побочные эффекты. Нужно, ни много ни мало, поискать индекс элемента в массиве имён свойств объекта, затем по найденному номеру изменить значение в другом массиве текущего состояния объекта. Что-то вроде такого кода: