Сегодня мы предлагаем вашему вниманию отрывок из книги Сергея Тарасова «Дефрагментация
Сокрытие базы данных или как скрестить ёжа с ужом
Упомянув один из крупнейших столпов современного софтостроения — мир ООП, нельзя обойти вниманием и другой — мир реляционных баз данных. Я намеренно вставил прилагательное «реляционные» применительно ко всем основным СУБД, хотя ещё в 1970-х годах такое обобщение было бы неправомерным.
Тем не менее, именно реляционным СУБД удалось в 1980-х годах освободить программистов от знания ненужных деталей организации физического хранения данных, отгородившись от них структурами логического уровня и стандартизованным языком SQL для доступа к информации. Также оказалось, что большинство форматов данных, которыми оперируют программы, хорошо ложатся на модель двумерных таблиц и связей между ними. Эти два фактора предопределили успех реляционных СУБД, а в качестве поощрительной премии сообщество получило строгую математическую теорию в основании технологии.
В отличие от реляционного мира, ООП развивалось инженерами-практиками достаточно стихийно, исходя из потребностей программистского сообщества, и потому никакой строгой теории под собой не имело. Имевшие место попытки подвести таковую под ООП задним числом терпели неудачу. Максимального результата добились авторы объявленного стандартом UML, который, однако, до сих пор в основном используется в качестве иллюстрирующих код картинок. Но лучше плохой стандарт, чем никакой.
И реляционная и объектная модели относятся к логическому уровню проектирования программной системы. Они ортогональны и по сути представляют собой два взгляда на одну и ту же сущность. Это значит, что вы можете реализовать ту же систему оставаясь в рамках только одного реляционно-процедурного подхода или же следуя исключительно ООП.
На практике сложилась ситуация, когда программы пишутся в основном с использованием ООП, тогда как данные хранятся в реляционных БД. Не касаясь пока вопроса целесообразности такого скрещивания «ёжа с ужом», примем ситуацию как данность. Из которой следует необходимость отображения (проецирования) объектов на реляционные структуры и обратно.
Ввиду упомянутого отсутствия под ООП формальной теоретической базы эта задача нерешаемая в общем случае, но выполнима в частных. Компонент программной системы, реализующий отображение, называется ORM (Object-Relational Mapping) или объектно-реляционный проектор — ОРП. Полноценный ORM может быть весьма нетривиальным компонентом, превышающим сложность остальной системы. Поэтому хотя многие разработчики с успехом пользуются своими собственными частными реализациями, в отрасли за последние 10 лет появилось несколько широко используемых фреймворков, выполняющих в том числе и задачу проекции.
Обзор средств объектно-реляционной проекции не планировался в рамках книги. Их и так достаточно в сети, включая небольшой мой собственный, сделанный ещё в 2005 году, но не сильно устаревший. Поэтому последующие примеры будут в основном касаться фреймворка NHibernate.
В технологии отображения объектов на РСУБД есть очень важный момент, от понимания которого во многом зависит успех вашего проекта. Я не раз слышал мнение программистов, что для слоя домена генерируемый проектором SQL является аналогом трансляции языка высокого уровня в ассемблер целевого процессора. Это мнение не просто глубоко ошибочно, но быстрыми темпами ведёт команду к созданию трудносопровождаемых систем с врождёнными и практически неисправимыми проблемами производительности.
Проще говоря, как только вы подумали о SQL, как о некоем ассемблере по отношению к используемому языку ООП, вы сразу влипаете в очень нехорошую историю.
SQL — высокоуровневый декларативный специализированный язык четвёртого поколения, в отличие от того же Java или C#, по-прежнему относящихся к третьему поколению языков императивных. Единственный оператор SQL на три десятка строк, выполняющий нечто посложнее выборки по ключу, потребует для достижения того же результата в разы, если не на порядок, больше строк на C#.
Такая ситуация приводит разработчиков ORM к необходимости создавать собственный SQL-подобный язык для манипуляции объектами и уже его транслировать в сиквел (HQL — Hibernate Query Language – SQL-подобный язык запросов, используемый в Hibernate/NHibernate ). Или использовать непосредственно SQL с динамическим преобразованием результата в коллекцию объектов.
В противном случае прикладной программист обречён на извлечение из БД и последующую обработку больших массивов данных непосредственно в своём приложении. Примерно так же обрабатывали табличные данные при отсутствии встроенного SQL разработчики на ранних версиях Clipper в конце 80-х годов. Там это называлось «навигационная обработка». Думаю, термин уместен и здесь.
В эпоху массового перехода с Clipper-подобных программ и файл-серверных технологий на клиент-серверные РСУБД многие приложения и их разработчики продолжали использовать навигационный подход. Приложения работали медленно, зачастую блокируя работу в многопользовательской среде. Потому что для эффективной работы с РСУБД нужно использовать подходы, ориентированные на обработку множеств на сервере, предполагающие наличие у разработчика умений работать с декларативными языками.
Тем не менее, получив в распоряжение ORM, программист зачастую возвращается к навигационным подходам обработки массивов данных вне РСУБД лишь с той разницей, что теперь этот массив, хочется надеяться, не представляет собой содержимое целой таблицы.
Почему? Недостаток знаний РСУБД пытаются заместить дополнительными уровнями абстракций. На деле же выходит обратное: уровни абстракции скрывают не детали слоя хранения объектов от программиста, а наоборот, его некомпетентность в области баз данных от СУБД. До некоторого времени.
Несмотря на толстый слой абстракций, предоставляемый ORM типа Hibernate, заставить приложение эффективно работать с РСУБД без знаний соответствующих принципов ортогонального мира и языка SQL практически невозможно.
Но попытки продолжаются. Одни по-прежнему разрабатывают проекторы для своих внутренних нужд, зачастую очень лёгкие. Другие ищут упрощение и выход в noSQL. Но в выигрыше пока остаются имеющие «базоданные» компетенции программисты и консультанты, зарабатывающие на тех, кто ими не обладает.
Как обычно используют ORM
На софтостроительных презентациях часто рисуют красивые схемы по разделению слоёв представления, бизнес-логики и хранимых данных. Голубая мечта начинающего программиста – использовать только одну среду и язык для разработки всех слоёв и забыть про необходимость знаний реляционных СУБД, сведя их назначение к некоей «интеллектуальной файловой системе». Слово SQL вызывает негативные ассоциации, связанные с чем-то древним, не говоря уже про триггеры или хранимые процедуры. На горизонте появляются добрые люди, с книгами разных гуру о домен-ориентированой разработке под мышкой, заявляющие новичкам примерно следующее: «Ребята, реляционные СУБД — пережиток затянувшейся эпохи 30-летней давности. Сейчас всё строится на ООП. И есть чудесная штука — ORM. Начните использовать ее и забудьте про тяжёлое наследие прошлого!»
Ребята принимают предложение. Дальше эволюция разработки системы примерно следующая.
Вначале происходит выбор ORM-фреймворка для отображения. Уже на этом этапе выясняется, что с теорией и стандартами дело обстоит плохо. Впору бы насторожиться, но презентация, показывающая как за 10 минут можно создать основу приложения типа записной книжки контактов очаровывает. Решено!
Начинаем реализовывать модель предметной области. Добавляем классы, свойства, связи. Генерируем структуру базы данных или подключаемся к существующей. Строим интерфейс управления объектами типа CRUD. Все достаточно просто. По крайней мере кажется вполне сравнимым с манипуляциями над DataSet. Тем кто о них знает, конечно, ведь не все подозревают о существовании табличных форм жизни данных в приложении за пределами сеток отображения DBGrid.
Как только разработчики реализовали CRUD-логику, начинается основное действо. Использовать сиквел напрямую теперь затруднительно. Не касаясь стратегий отображения и проблем переносимости приложения между СУБД, по сути каждый SQL-запрос с соединениями, поднявшись в домен, сопровождается специфической проекцией табличного результата на созданный по этому случаю класс. Поэтому приходится использовать собственный язык запросов ORM. Нестандартный, без средств отладки и профилирования. Если он, язык, вообще имеется в данном ORM. Для поддерживающих соответствующую интеграцию среда .NET даёт возможность использовать LINQ, позволяющий отловить некоторые ошибки на стадии компиляции.
Сравните выразительность языка на простом примере, который я оставлю без комментариев.
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-запросов к СУБД при такой обработке коллекций может исчисляться десятками тысяч.
Автор: ph_piter