Для современного программного обеспечения характерно выделение функциональности по хранению и управлению данными в отдельный слой, а СУБД выполняют общие для любых типов хранилищ задачи: физическое размещение данных, доступ к данным с использованием запросов, обеспечение целостности данных и устойчивости к сбоям. Таким образом, типичная система разделена на две части: бизнес-логику прикладного приложения и СУБД. Одни и те же СУБД — и, более того, одни и те же экземпляры баз данных — могут использоваться в разных приложениях, и как следствие возникают вопросы, связанные с унифицированным интерфейсом доступа к функциям СУБД.
SQL
Базовым способом доступа к функциональности реляционных СУБД является использование одного из диалектов SQL — прикладное приложение формирует текст SQL-запроса (запрос на выборку либо на модификацию данных) и передает его в СУБД с использованием специального программного интерфейса. Результат, например отфильтрованные данные, возвращается в приложение.
После подключения к соответствующему экземпляру базы данных формируется текст SQL-запроса, например SELECT-запрос на выборку данных, и здесь важно обратить внимание на то, что текст запроса задается как строка. При обработке результата запроса требуется преобразовать табличные данные, которые возвращает СУБД, например, в объектную модель приложения. Аналогичным образом производится вызов хранимых процедур базы данных. В этом случае требуется передавать имя процедуры в виде текста.
Непосредственное использование SQL-запросов в коде прикладного приложения обязывает разработчика управлять данными на уровне их реляционной модели и самостоятельно обеспечивать отображение этих данных на объектную модель приложения. Тем не менее этот способ остается популярным. РСУБД и SQL успешно применяются уже многие годы и доказали свою эффективность при решении конкретных задач, поэтому, когда стоит выбор между классической базой с SQL-интерфейсом и другой альтернативой, то, возможно, в большинстве случаев следует остановиться на проверенном временем решении. Часто над этим выбором даже не задумываются, полагаясь на опыт предыдущих проектов, однако не стоит забывать про активно развивающиеся сейчас объектно-ориентированные языки программирования, предоставляющие разработчикам богатые возможности.
Средства ORM
Язык SQL полагается на реляционную модель, и как следствие возникают сложности при его использовании в системах, написанных на объектно-ориентированных языках. Для смягчения этих проблем предлагаются так называемые средства объектно-реляционного отображения (object-relational mapping, ORM), которые строятся поверх SQL и предоставляют разработчику интерфейс для работы в рамках объектно-ориентированного языка (например, C#). Разработчик может манипулировать данными в терминах объектной, а не реляционной модели.
Ключевым преимуществом использования объектно-ориентированных языков по сравнению с текстовыми запросами к СУБД является строгая типизация модели данных: любой объект модели данных имеет строго определенный тип; все операции над объектами модели имеют набор параметров с определенными типами, а соответствие типов формальных и фактических параметров проверяется на этапе компиляции. В случае использования текстовых SQL-запросов отсутствует как строгая типизация запросов (они задаются в виде строки), так и строгая типизация возвращаемых данных, а возвращаемые данные представлены в виде двухмерных таблиц без привязки к объектной модели приложения. Такой подход не защищает прикладного программиста от возможных ошибок в тексте запроса и в наименованиях элементов структуры данных. Кроме того, изменения модели данных вызывают каскадное изменение модели приложения и кода отображения, что может стать источником новых ошибок.
Средствами ORM реализована Java-библиотека Hibernate, предназначенная для решения проблемы объектно-реляционного отображения. Для языков семейства. NET также существует своя версия библиотеки — NHibernate. Приведем пример использования этой библиотеки на языке C#. Пусть в прикладном приложении определен класс Customer, являющийся частью объектной модели. В этом классе определены два поля: Id целочисленного и Name строкового типов. Допустим, что в целевой базе данных определена таблица CUSTOMERS с соответствующими полями CID и NAME. Для функционирования Hibernate необходимо создать специальный XML-файл с описанием отображения:
-
- assembly="Test"
- namespace="Test.Models">
-
-
-
-
Также необходимо создать XML-файл с общими настройками доступа Hibernate к экземпляру базы данных.
В Hibernate имеется возможность указывать свойства отображения, используя атрибуты языка программирования, что во многих случаях оказывается более удобно. XML-файл, задающий отображение, в данном случае не требуется. Как можно было ожидать, управление данными происходит на уровне объектной модели, например, можно использовать объект, который поддерживает список объектов, участвующих в текущей транзакции, это обеспечивает атомарное изменение данных объектов в базе и разрешает проблемы с параллельным доступом к данным.
Библиотека Hibernate позволяет обрабатывать все возможные связи между объектами баз данных, в том числе: ассоциации — простые связи между объектами, внешние ключи в реляционной модели, ссылки на объект другого класса в качестве поля исходного класса в объектной модели; наследование. В реляционной модели понятия наследования нет, поэтому возможны разные варианты отображения иерархии наследования на реляционную структуру данных.
Библиотека Hibernate позволяет разработчику работать с данными на уровне объектной модели, это значительно упрощает написание кода доступа к данным. К преимуществам библиотеки можно отнести и возможность создания отображения для уже существующих баз данных без изменения их структуры, что часто является необходимым требованием. Однако имеются и свои недостатки: сложность создания и поддержки XML-файлов отображения, а также потенциальная негибкость отображения в случае сложных сценариев работы с данными.
Что касается строгой типизации, то модель данных при использовании Hibernate описывается в терминах объектно-ориентированного языка (Java или C#), а значит, является строго типизированной. Однако для задания объектно-реляционного отображения требуется поддерживать схему этого отображения: либо в виде XML-файла, либо в виде атрибутов элементов языка программирования. Такие схемы отображения вполне могут создаваться автоматизированными средствами, поэтому тот факт, что схема использует текстовые строки как ссылки на элементы модели данных, нельзя считать недостатком.
Запросы к данным с использованием Hibernate строго не типизированы; например, требуется в виде строки указывать имена полей модели, которые участвуют в запросе. С другой стороны, запросы Hibernate принципиально отличаются от SQL-запросов — они формулируются в терминах объектной, а не реляционной модели, что представляет значительные удобства для прикладного программиста.
Entity Framework (EF) – аналог библиотеки Hibernate в версии Microsoft. Для EF характерны те же преимущества и недостатки, что и для Hibernate. Средства системы разработки Visual Studio, а также возможности интегрированных в язык запросов (LINQ) позволяют разработчику создавать запросы в более удобной «функциональной» форме. При этом обеспечивается строгая типизация.
Возможность использования запросов LINQ со строгой типизацией — серьезное преимущество EF по сравнению с Hibernate. Во-первых, это строгая типизация запросов к данным. Во-вторых, сам запрос синтаксически повторяет структуру типичного SQL-запроса. Такая форма более наглядна, чем в случае с Hibernate. При этом, естественно, остается ключевое преимущество Hibernate – запрос задается в терминах объектной модели. Также в среду разработки Visual Studio входят средства для генерации классов модели по существующей базе данных и, наоборот, схемы базы данных по классам модели.
В итоге, используя EF, прикладной разработчик имеет в своем распоряжении строго типизированную модель, возможность выполнять строго типизированные запросы к данным и возможность автоматизированно задавать детали объектно-реляционного отображения.
LINQ to SQL – упрощенный вариант Entity Framework, идеально подходящий в случаях, когда база данных создается с нуля. Однако этот инструмент не подходит, когда необходимо создать отображение на объектную модель уже существующей базы данных и при наличии сложных схем отображения.
Рассмотренные варианты ORM на уровне архитектуры реализуют типовые решения: преобразователь данных (Data Mapper) и единица работы (Unit of Work). Другой подход используется в среде разработки Ruby on Rails, в которой применяется типовое решение — активная запись (Active Record). Язык программирования Ruby является динамическим, что предоставляет дополнительные удобства при работе с источниками данных. В самом простом варианте разработчику вовсе не требуется задавать свойства отображения. Также предполагается, что имя таблицы в базе соответствует имени класса, а имя колонки таблицы соответствует имени поля класса. Синтаксис запросов очень близок к SQL, хотя задается в большинстве случаев в терминах объектной, а не реляционной модели.
Основное преимущество Active Record в Ruby On Rails — простота: не требуется задавать свойства отображения в виде XML, количество кода минимально. Основные недостатки: сложность работы с уже существующими базами или с большими базами со сложной структурой, отсутствие проверки типов до этапа запуска приложения.
Объектно-ориентированные базы данных
Объектно-ориентированные СУБД (ООСУБД) полностью отказываются от реляционной модели, что решает проблему объектно-реляционного отображения. Интерфейс для работы с такими базами данных наиболее естествен для разработчиков программных систем, использующих объектно-ориентированные языки.
Для начала создадим экземпляр интерфейса IObjectContainer. Это объект, выполняющий ту же роль, что и session в Hibernate или context в EF. Самый простой способ создания этого объекта – открыть существующий или создать новый локальный файл базы данных:
- IObjectContainer db = Db4oFactory.OpenFile(filename);
Далее можно производить выборки объектов (SELECT):
-
IList
result = db.Query (cust => cust.Name.StartsWith("A"));
Добавлять новые записи (INSERT), изменять существующие (UPDATE), удалять существующие (DELETE):
- Customer customer = new Customer { Name = «Google» };
- db.Store(customer); // INSERT
- customer.Name = "Sun";
- db.Store(customer); // UPDATE
- db.Delete(customer); // DELETE
В целом интерфейс работы с данными с ООСУБД аналогичен интерфейсу работы с данными при использовании рассмотренных выше средств объектно-реляционного отображения, в частности Entity Framework. Основное отличие – это отсутствие необходимости в реализации объектно-реляционного отображения на уровне библиотеки. Как следствие нет необходимости в поддержке целостности отображения. Изменение модели данных не приводит к цепным изменениям в настройках отображения и в самой базе.
С точки зрения прикладного разработчика интерфейс ООСУБД является оптимальным: обеспечивается работа с классами модели данных, а не с реляционными структурами; обеспечивается строгая типизация модели данных; обеспечивается строгая типизация запросов к данным.
***
Лобовым методом работы с базами данных является использование прямых SQL-запросов, однако основной недостаток этого подхода — необходимость отображения реляционных данных на объектную модель приложения. Интерфейс разработчика в этом случае изначально задается в терминах реляционной модели данных.
Использование средств объектно-реляционного отображения, а также ООСУБД позволяет получить строго типизированный интерфейс в терминах объектной модели приложения. Недостатком средств ORM при этом является необходимость в поддержке целостности отображения. Недостаток ООСУБД — невозможность их интеграции с уже существующими базами данных. Особыми свойствами обладают специализированные средства ORM в динамических языках программирования, которые, в частности, позволяют избежать необходимости в поддержании отдельных файлов отображения. С другой стороны, эти решения не обладают многими преимуществами, связанными со строгой типизацией.
Алексей Богданов (alxbog@gmail.com) – аспирант факультета ВМиК МГУ (Москва).