1 Задачи по реализации библиотек доступа к реляционным БД – «надстроек» над JDBC (как правило, задача данного типа альтернативна задаче по созданию утилит) Задачи имеют целью знакомство с общей культурой проектирования доступа к данным в приложениях, а также изучение JDBC как примера интерфейса уровня обращения. Конкретнее, задачи состоят в реализации на Java библиотек, позволяющих работать с базами данных в произвольных приложениях без знания JDBC и SQL. Кроме того, в них иллюстрируется построение библиотек доступа к абстрактным хранилищам данных, что часто используется при проектировании реальных информационных систем. Общие замечания 1. Таблицы должны создаваться и редактироваться в схеме пользователя, имя которого является английским написанием фамилии выполняющего задание (если требуется несколько схем, к фамилии следует добавлять номер). 2. Класс(ы) должны быть помещены в отдельный пакет ru.ipccenter.db.фамилия.имя_задачи, где имя_задачи рекомендуется ниже для каждой задачи. 3. Реализованная библиотека должна быть проверена с помощью отдельного класса(ов) (*Test) с методом main(), который задает параметры соединения с конкретной базой данных и выполняет несколько операций чтения/записи из БД, распечатывая результаты в консоли. 4. Указанный ниже уровень сложности задач — по 10-балльной системе. Число баллов за задачу вычисляется по формуле 5*(сложность – число недочетов), т.е. максимум – 50 баллов (но, как правило, максимум недостижим). 1 Библиотеки, упрощающие доступ к реляционным БД Данные варианты библиотек являются различными реализациями одних и тех же интерфейсов, заданных в пакете ru.ipccenter.db. Эти интерфейсы могут быть реализованы не только через JDBC и даже не только через реляционные БД: они задают доступ к произвольным данным, представимым в виде набора таблиц. Упрощение использования такой библиотеки по сравнению с JDBC состоит в сокращении объема вызывающего кода и в отсутствии необходимости знания SQL. 1.1 (“ru.ipccenter.db.фамилия).jdbc”, «обычные SQL-инструкции» Реализация инструкций DML и простейшего DQL в библиотеке для программистов, не знающих SQL и JDBC (такая библиотека по отношению к JDBC называется «адаптером», если смотреть на нее с точки зрения шаблонов программирования). Требующий реализации через JDBC интерфейс ru.ipccenter.db.Table отвечает за выполнение простейших операций выборки 2 данных (выборка всех строк, выборка строк и отдельных полей по ключу, по атрибуту(ам)) из определенной таблицы и за изменение ее данных (вставка строки, удаление строки по ключу, обновление атрибута, обновление всех неключевых атрибутов). В методы выборки всех строк и выборки по атрибуту передается дополнительный аргумент для сортировки строк. Извлекаемые и записываемые данные представляются в виде массива объектов (Object[]). Класс Criterion, используемый при выборке, хранит информацию об условии отбора данных по значению одного атрибута (значение и оператор сравнения с ним: ==, !=, > и т.д.), а класс Criteria — об условиях произвольной выборки. При реализации интерфейса Table в классе JDBCTable следует воспользоваться одним и тем же Statement для всех типов операций. Для простоты можно предположить, что в любой таблице первый столбец – ID INT PRIMARY KEY. При вставке (INSERT) значение первичного ключа генерируется автоматически (для этого JDBCTable хранит максимальное значение ключа, что позволяет не зависеть от способа генерации значений, принятых в разных СУБД). Класс JDBCDatabase — реализация интерфейса Database — отвечает за соединение с БД, за создание экземпляров Table (в соответствии с шаблоном программирования Factory) и их кэширование в памяти (по именам, — например, с помощью HashMap). Можно унаследоваться от класса AbstractDatabase (частичная реализация интерфейса Database с кэшированием таблиц). Классы TableInfo и ColumnInfo помогают хранить информацию о таблице и её столбцах (она может запрашиваться из БД при создании объекта JDBCTable, причём информация о типе данных хранится в стандарте JDBC: см. java.sql.Types). Преобразование данных (только простых типов) к классам Java и обратно (с учетом информации о типах из TableInfo) желательно сделать обязанностью специального интерфейса (TypeConverter; его нужно реализовать для Oracle), экземпляр которого нужно устанавливать в JDBCTable. Сложность: 4. 1.2 “*.jdbc.lob”, «операции с LOB-объектами» То же самое, что и в 1.1, но с операциями над LOB-объектами. Для упрощения следует обойтись без метаданных. Данные Java простых типов (точнее, объекты класса String и классовоболочек типа Integer) при вставке и обновлении строк (а также при задании условий выборки) в отсутствие метаданных можно преобразовывать к строке методом toString(), независимо от типа столбца (который неизвестен). Только для LOB-атрибутов при их передаче в БД определяются тип — по передаваемому в методы вставки и обновления значению (с помощью оператора instanceof; например, такие объекты можно передавать как InputStream или (для CLOB) StringBuffer). Под LOB-объектами здесь понимаются «настоящие» BLOB и CLOB, а не LONGтипы; способ работы с ними сильно зависит от СУБД (если только не использовать драйвер JDBC 2.0, который, однако, имеется не для всех СУБД). При обратном преобразовании (из ре- 3 зультатов выборки из БД в объекты Java) достаточно предположить только целые, вещественные числа и LOB-объекты, а все остальное считать строками. Сложность: 6. 1.3 “*.jdbc.prepared”, «подготовленные SQL-инструкции» То же самое, что и в 1.1, но с использованием объектов PreparedStatement для разных типов запросов (кроме выборки по многим атрибутам; для выборки по одному атрибуту и для обновления одного атрибута создавать отдельный PreparedStatement на каждый атрибут). Желательно создавать PreparedStatement не при создании JDBCTable, а по мере необходимости перед выполнением соответствующего запроса, после чего кэшировать PreparedStatement (запоминать в каком-либо поле класса JDBCTable). Сложность задачи можно уменьшить, если не использовать метаданные (при установке значений можно воспользоваться методом setObject() вместо setInt(), setString() и т.п.). Сложность: 6 (5). 1.4 “*.jdbc.call”, «вызов хранимых процедур» То же самое, что и в 1.1, но с использованием CallableStatement и хранимых процедур для выполнения операций обновления (это, конечно, нетипичное применение хранимых процедур: никакого улучшения производительности в этой учебной задаче не достигается). В хранимую процедуру невозможно передать нетипизированное значение (Object) или массив значений разных типов. В связи этим преобразование типов данных должно происходить не в хранимых процедурах, получающих готовые строковые значения и вставляющих их в SQL-инструкцию, а на стороне клиента (метаданные при этом для простоты использовать не нужно, аналогично 1.2). Обновление всех атрибутов реализуется посредством многократного вызова метода обновления отдельного атрибута. Процедуры предполагается писать не на Oracle PL/SQL, а на Java (поддерживается с версии Oracle8i). Следует использовать целочисленные значения, возвращаемые при вызове хранимых процедур, как признак успеха операции. Операции вставки и удаления строки можно сделать с помощью PreparedStatement, а операции выборок (DQL) следует сделать аналогично 1.1, то есть путем формирования строк SQL непосредственно перед запросом. Сложность: 8. 2 Библиотеки доступа к реляционным БД через объекты (фиксированного класса) Описываемые здесь библиотеки являются различными реализациями одних и тех же интерфейсов, заданных в пакете ru.ipccenter.odb. Эти интерфейсы могут быть реализованы не только через JDBC и не только через реляционные БД: они задают доступ к любым хранилищам объектов. Интерфейсы иллюстрируют основные возможности объектных БД, хотя представляют только один из возможных путей хранения объектов — хранение полей объектов на 4 стороне клиента в коллекциях (с обращением к ним по именам полей). Другие пути (более простые для приложений, но менее гибкие и производительные) описаны в разделе 3. 2.1 “(ru.ipccenter.odb.фамилия.jdbc).data”, «объектная надстройка над JDBC» Реализация доступа к строкам таблицы как объектам (некоторого интерфейса PersistentData). Требующий реализации через JDBC интерфейс ru.ipccenter.odb.DataTable (объектный аналог описанного в 1.1 интерфейса Table) отвечает за выполнение простейших операций выборки объектов (выборка всех, по ключу, по полю(ям)) из определенной таблицы и за изменение ее данных (создание объекта, удаление объекта по ключу, обновление его поля, сохранение всех полей объекта). В методы выборки всех объектов и выборки по полю передается дополнительный аргумент для сортировки объектов. Извлекаемые и записываемые данные представляются в виде объектов интерфейса PersistentData (реализован классом DefaultPersistentData), который содержит набор поименованных полей (с типизированными методами доступа на чтение и на изменение), а также методы для удаления и сохранения себя. В конструктор DefaultPersistentData в качестве содержимого (MutableData contents) предлагается передавать экземпляр класса ru.ipccenter.data.ArrayData (это «обертка» для массива полей). Объекты PersistentData должны кэшироваться JDBCDataTable во избежание использования приложением нескольких копий одной строки таблицы (текущие значения полей объектов PersistentData не обязаны совпадать с содержимым базы данных, пока для них не будет вызван метод сохранения). Для хранения условий выборки и метаинформации о таблице используются те же классы Criterion, Criteria, TableInfo и ColumnInfo, которые были описаны в 1.1. Другие тонкости реализации через JDBC интерфейсов DataTable и DataBase (в классе JDBCDataTable и JDBCDataBase) аналогичны описанной там же реализации интерфейсов Table и Database. Сложность: 7. 2.2 “*.data.swizzle”, «объектная надстройка над JDBC с подкачкой полей» То же самое, что и в 2.1, но значения полей загружать в объекты PersistentData не сразу, а при первом обращении к ним (такая загрузка называется swizzling, «подкачка»). Смысл этого: приложения обычно сразу загружают и представляют пользователю в лучшем случае только названия объектов, а их содержимое показывается (и редактируется) гораздо реже, чем названия; поэтому данный подход намного экономичнее по памяти (хотя хуже по скорости). Желательно поддерживать как однократную подкачку всего содержимого объекта (инициализацию поля contents класса DefaultPersistentData), так и подкачку по одному полю. Для реализации необходимо унаследовать класс (SwizzleData) от DefaultPersistentData и переопределить в нем getContents() и методы чтения полей. В них необходимо проверять, загружено уже поле или нет, путем его сравнения с каким-либо специальным значением (константой (равной, например, 5 Byte.TYPE), которая заведомо не может существовать в БД). Задачу можно упростить за счет отсутствия кэширования объектов. Сложность: 8 (7). 2.3 “*.data.ref”, «объектная надстройка над JDBC с полями-ссылками» То же самое, что и в 2.1, но в предположении, что поля объекта PersistentData могут не только иметь простой тип, но и быть ссылками на другие объекты PersistentData. Загружать такие поля необходимо лишь при первом обращении к ним (аналогично 2.2), переопределив метод чтения поля-ссылки в классе RefFieldData, наследующего DefaultPersistentData. В соответствующем методе JDBCTable необходимо воспользоваться загруженной из DatabaseMetaData информацией об импортируемых таблицей ключах (о таблицах, из которых загружать ссылки). Задачу можно упростить за счет отсутствия кэширования объектов. Сложность: 9 (8). 2.4 “*.data.array”, «объектная надстройка над JDBC с полями-массивами» То же самое, что и в 2.1, но в предположении, что поля объекта PersistentData могут не только иметь простой тип, но и быть массивами других объектов PersistentData. Загружать такие поля необходимо лишь при первом обращении к ним (аналогично 2.2), переопределив метод чтения поля-массива в классе ArrayFieldData, наследующего DefaultData. В соответствующем методе JDBCTable необходимо воспользоваться загруженной из DatabaseMetaData информацией об экспортируемом таблицей ключе (о таблице, из которой загружать массив). Желательно рассматривать только такие экспортируемые ключи, у которых установлено каскадное удаление (именно такие сильные связи, как правило, устанавливаются между родительской и дочерней таблицами). К сожалению, в связи с недостаточностью метаинформации невозможно иметь в таблице несколько полей-массивов, а также сделать поля-массивы упорядоченными, указывая способ сортировки в выборке при загрузке поля-массива (хотя все это поддерживается объектными базами данных). Задачу можно упростить за счет отсутствия кэширования объектов. Сложность: 9 (8). 3 Библиотеки доступа к реляционным БД через бизнес-объекты (разных классов) Описываемые здесь библиотеки являются различными реализациями одних и тех же интерфейсов, заданных в пакете ru.ipccenter.odb. Эти интерфейсы могут быть реализованы не только через JDBC и не только через реляционные БД: они задают доступ к любым хранилищам объектов. Интерфейсы иллюстрируют основные возможности объектных баз данных, которые предоставляют доступ к данным через getter/setter методы зависящих от структуры БД классов (называемых бизнес-объектами). Реализации делятся на следующие группы (здесь для каждого варианта реализации приводится аналогичная технология из Java Access Classes (JAC) – библиотеки объектного доступа к Oracle Lite): 6 1) хранение данных на стороне клиента в «настоящих» полях «обычных» (transient) объектов (с обращением к ним через Reflection API) — аналог Simple Stateful Persistence в JAC; 2) хранение данных на стороне клиента в «настоящих» полях хранимых (persistent) объектов («знающих» о структуре соответствующих таблиц) — аналог Seamless Stateful Persistence; 3) хранение данных только в базе данных и обращение к ним на стороне клиента через не имеющие состояния (stateless) объекты («знающие» о структуре соответствующих таблиц) — аналог Stateless Persistence в JAC. 3.1 «(ru.ipccenter.odb.фамилия.jdbc).reflect», «transient-объектная надстройка над JDBC» В отличие от 2.1, данные здесь хранятся не в коллекции (массиве) внутри объектов общего интерфейса PersistentData (с доступом по строковому имени поля на этапе выполнения программы), а в объектах разных классов (в так называемых бизнес-объектах), имеющих «настоящие» поля, наличие которых проверяется на этапе компиляции программы. Каждый класс бизнес-объекта соответствует одной таблице и имеет такое же название (с целью соблюдения правил именования классов и полей в Java желательно в названиях таблиц и их атрибутов учитывать регистр символов, что достигается путем использования “закавыченных” (quoted) имен в БД). По сравнению с 2.1, это ускоряет разработку конкретных систем и уменьшает число ошибок, однако при каждом изменении в структуре базы данных требуется изменять структуру бизнес-объектов (на практике эта проблема частично решается с помощью средств автоматической генерации бизнес-объектов по структуре базы данных или наоборот). В отличие от рассмотренных выше библиотек, здесь используются несколько другие интерфейсы пакета ru.ipccenter.odb: BusinessObject, ObjectTable, ObjectBase вместо PersistentData, DataTable, DataBase, соответственно. Интерфейс BusinessObject, в отличие от PersistentData, не задает доступ к полям объекта, имея только методы удаления и сохранения себя. Интерфейс ObjectTable отличается от DataTable только типом возвращаемых и получаемых методами значений: BusinessObject вместо PersistentData. В данной реализации (по сравнению с описываемыми в 3.5 и ниже) обмен данными между атрибутами таблиц и полями объектов в классе JDBCObjectTable следует реализовывать в предположении совпадения их имен с помощью Reflection API (пакет java.lang.reflect), которое позволяет обращаться к методам объектов по именам (см. приложение 2). При этом не предполагается реализация BusinessObject (наследование AbstractBusinessObject) в конкретных (содержащих поля) классов бизнес-объектов (эти классы остаются не хранимыми сами по себе, т.е. transient). Вместо этого объекты этих классов должны «обертываться» (вкладываться) в proxyобъекты класса ReflectJDBCBusinessObject, который должен наследоваться от AbstractBusinessObject, работать через Reflection cо вложенным в него объектом, иметь методы установки и 7 предоставления значений всех полей вложенного объекта (эти методы будет вызывать JDBCObjectTable; желательно не передавать значения полей в обе стороны через массивы объектов – это сказывается на производительности). Сложность: 9. 3.2 “*.reflect.swizzle”, «transient-объектная надстройка над JDBC с подкачкой полей» То же самое, что и в 3.1, но с подкачкой полей, как в 2.2. Задачу можно упростить за счет отсутствия кэширования объектов. Сложность: 10(9). 3.3 “*.reflect.ref”, «transient-объектная надстройка над JDBC с полями-ссылками» То же самое, что и в 3.1, но с полями-ссылками, как в 2.3. Методы доступа к этим полям должны иметь класс ссылки в качестве типа получаемого и возвращаемого значения. Задачу можно упростить за счет отсутствия кэширования объектов. Сложность: 11(10). 3.4 “*.reflect.array”, «transient-объектная надстройка над JDBC с полями-массивами» То же самое, что и в 3.1, но с полями-массивами, как в 2.4. Методы доступа к этим полям должны иметь соответствующий класс (не Object[]) в качестве типа получаемого и возвращаемого значения. Задачу можно упростить за счет отсутствия кэширования объектов. Сложность: 11(10). 3.5 “(ru.ipccenter.odb.фамилия.jdbc).stateful”, «stateful-объектная надстройка над JDBC» То же самое, что в 3.1 (первый абзац), однако данные хранятся в тех же классах, которые взаимодействуют посредством JDBCObjectTable с базой данных, а не в произвольных (transient) классах. Другими словами, зависящие от структуры таблиц классы бизнес-объектов наследуются от AbstractBusinessObject (точнее, от JDBCBusinessObject, который имеет упомянутые в 3.1 методы установки и предоставления значений всех полей). При этом эти классы бизнесобъектов «знают» соответствующие их полям имена атрибутов таблиц. (На практике в такой ситуации часто используется автоматическая генерация бизнес-объектов по структуре базы данных, создающая поля с именами, равными именам атрибутов таблицы). Данная задача проще 3.1 (хотя бы потому, что ответственность за отображение полей в атрибуты и наоборот перекладывается с BusinessObject на конкретные классы, не являющиеся частью библиотеки). Слово “stateful” («имеющий состояние объект») в названии данной задачи отражает ее отличие от 3.9, где данные вообще не хранятся на стороне клиента. Сложность: 8. 3.6 “*.stateful.swizzle”, «stateful-объектная надстройка над JDBC с подкачкой полей» То же самое, что и в 3.5, но с подкачкой полей, как в 2.2. Задачу можно упростить за счет отсутствия кэширования объектов. Сложность: 9(8). 8 3.7 “*.stateful.ref”, «stateful-объектная надстройка над JDBC с полями-ссылками» То же самое, что и в 3.5, но с полями-ссылками, как в 2.3. Методы доступа к этим полям должны иметь класс ссылки в качестве типа получаемого и возвращаемого значения. Задачу можно упростить за счет отсутствия кэширования объектов. Сложность: 10(9). 3.8 “*.stateful.array”, «stateful-объектная надстройка над JDBC с полями-массивами» То же самое, что и в 3.5, но с полями-массивами, как в 2.4. Методы доступа к этим полям должны иметь соответствующий класс (не Object[]) в качестве типа получаемого и возвращаемого значения. Задачу можно упростить за счет отсутствия кэширования объектов. Сложность: 10(9). 3.9 “(ru.ipccenter.odb.фамилия.jdbc).stateless”, «stateless-объектная надстройка над JDBC» То же самое, что в 3.1 (первый абзац), однако данные извлекаются и модифицируются через те же классы, которые взаимодействуют посредством JDBCObjectTable с базой данных, а не через произвольные transient классы (см. пояснения в аналогичной задаче 3.5). Слово “stateless” («не имеющий состояние объект») в названии данной задачи отражает ее отличие от 3.5. Здесь данные вообще не хранятся на стороне клиента («тонкий клиент»), а извлекаются из БД при любом обращении к getter-методам (т. е. они «подкачиваются» всякий раз). С другой стороны, модификация данных в БД происходит всякий раз, когда вызывается setter-метод (save() не имеет смысла). Как следствие, данный вариант библиотеки имеет значительно меньшую производительность; однако он проще для реализации в связи с отсутствием большой необходимости в кэшировании объектов. Сложность: 7. То же самое, что и в 3.5, но с полями-ссылками, как в 2.3. Методы доступа к этим полям должны иметь класс ссылки в качестве типа получаемого и возвращаемого значения. Сложность: 9. 3.10 “*.stateless.ref”, «stateless-объектная надстройка над JDBC с полями-ссылками» То же самое, что и в 3.5, но с полями-ссылками, как в 2.3. Методы доступа к этим полям должны иметь класс ссылки в качестве типа получаемого и возвращаемого значения. Сложность: 9. 3.11 “*.stateless.array”, «stateless-объектная надстройка над JDBC с полями-массивами» То же самое, что и в 3.5, но с полями-массивами, как в 2.4. Методы доступа к этим полям должны иметь соответствующий класс (не Object[]) в качестве типа получаемого и возвращаемого значения. Сложность: 9. 9 Примечание Все рассмотренные варианты объектного доступа к реляционным БД могут быть элементарно перенесены с клиент-серверной на трёхуровневую архитектуру; при этом описанные там классы обычно работают в приложении промежуточного уровня (выполняют запросы к БД и осуществляют кэширование извлеченных оттуда объектов). Если реализовывать стандартную трехуровневую архитектуру (с «тонкими клиентами»), то на стороне клиентов нужно воспользоваться теми же самыми интерфейсами, в частности, так, чтобы при любом изменении в клиентском приложении поля объекта (PersistentData или BusinessObject) обращаться к серверу по сети (например, через сокеты (java.net) или через удаленный вызов методов (java.rmi)). ((В трехуровневой архитектуре с «толстыми клиентами» именно клиентские приложения хранят данные в своих объектах и самостоятельно извлекают эти объекты из базы данных (как описано выше). Единственное назначение сервера в этом случае заключается в согласовании между всеми клиентами результатов операций создания, удаления объектов и изменения их полей.)) Приложение (к задачам 3.1–3.4). Метод, загружающий строку таблицы в public-поля произвольного объекта, которые имеют такие названия, что и столбцы таблицы. protected void loadDataFromSet(ResultSet set) throws SQLException { ResultSetMetaData meta = set.getMetaData(); int n = meta.getColumnCount(); for (int i=0; i<n; i++) try { this.getClass().getField(meta.getColumnLabel(i)).set(this, set.getObject(i)); } catch(NoSuchFieldException e1){ } catch(SecurityException e2){ } catch(IllegalArgumentException e3){ } catch(IllegalAccessException e4){ } }//вместо set()+getObject() лучше setInt()+getInt() и т.п. (самостоятельно преобразуя типы) Метод, записывающий public-поля произвольного объекта в объект java.util.HashMap (key – название поля, value – значение поля). (Такая структура может использоваться как для вставки строки, так и для изменения ячеек таблицы.) protected final HashMap getDataAsHashMap() { Field[] fields = this.getClass().getFields(); HashMap result = new HashMap(fields.length, 1.0); for (int i=0; i<fields.length; i++) try { result.put(fields[i].getName(), fields[i].get(this)); } catch(IllegalArgumentException e1){ } catch(IllegalAccessException e2){ } }