Вот здесь можно узнать больше об базе данных cloudscape. Часть 1
Есть такая замечательная фирма ibm и делает она базу данных cloudscape (вот ее адрес http://www.ibm.com/software/data/cloudscape/). Вообще-то, ibm купила разработчиков cloudscape еще лет 5-ть назад, но сути дела это не меняет. Т.к. база для мира java очень хорошая, и я ей пользовался некоторое время назад.
Признаюсь что писал данный материал я еще в далеком детстве года, эдак, 4 назад. Поэтому мой литературный стиль еще не так отточен, как хотелось, текст сыроват, плоховат, и наверняка сильно устарел, но раз уж я его нашел среди своих старых материалов, то почему бы и нет.
Для любого высокоуровневого языка программирования необходимым условием успеха является качественная поддержка баз данных или за счет использования специфических языковых конструкций или благодаря использованию отлаженной модели объектов доступа к данным, например, “microsoft DAO”. Очевидно, что на соверменном этапе развития инструментальных средств разработчики стремятся к унификации инструментария и методов доступа к различным источникам данных, а в отдельных ситуациях, например, при разработке приложений функционирующих в гетерогенных средах и к интеграции разнородных источников данных.
И хотя при разработке простых desktop-приложений такие потребности часто и не возникают, согласитесь, что глупо в простейшей программе учета грабель на складе хранить часть информации в БД Sybase или Oracle, часть в mysql или paradox, а третью часть представлять в виде xml-документов, но разработчики мигрируют в сторону унификации средств и методов работы с БД. Так в свое время появилось гениальное изобретение ODBC, обеспечивающее единый API-интерфейс доступа к самым разнообразным БД, разработанный комитетами X/OPEN и SQL Access Group и получивший первое имя стандарт CLI (Call Level Interface). Когда данная технология только-только появлялась международные организации ANSI и ISO уже работали в направлении стандартизации SQL для различных СУБД и начало работы по CLI с целью стандартизации доступа к различным БД стало важной вехой на пути стандартизации интерфейсов программирования.
Т.к. физическая реализация данного стандарта идет через использование специальных библиотек или драйверы БД, которые должны находиться в ОП во время работы приложения, то очевидно схема по которой приложение нуждаясь в услугах доступа к БД загружает соответствующий драйвер, т.е. очевидно, что приложение может без перекомпиляции получать не только доступ к различным источникам данных, но и к нескольким из них одновременно. Кроме того следует помнить, что любая технология изменяется и делать ставки на использование быстро изменяющихся родных интерфейсов доступа к данным несколько рискованно. Идеология ODBC заключается в существовании одного менеджера драйверов и некоторого количества ODBC-драйверов понимающих специфику конкретной БД.
Следующим этапом процесса унификации становится использование технологий OLE DB, ключевой технологии Microsoft, позволяющей стандартизировать доступ не только к реляционным БД, но и к другим типам источников данных. Мы получаем возможность работы с любыми данными где бы и в каком бы формате они не находились.
Основные моменты на которые должен будет обратить внимание разработчик java это:
- установление соединения с БД;
- выборка данных, использование конструкций DML и DDL с помощью обычных statements
или prepared statements или с помощью хранимых процедур;
- работа с наборами данных (resultset);
- обработка исключений и предупреждений SQL.
Предполагается наличие соответствующих знаний инструкций SQL, и стандартизированных типов данных в соответствии с SQL-92 и отображение между типами данных SQL и типами данных java. Большая часть ведущих СУБД соответствует данному стандарту и, как же без этого, дополняющих своими уникальными возможностями стандартный SQL.
Разработчик java знает основные стадии работы с БД. Первым шагом идет загрузка и регистрация конкретного драйвер БД, если конечно необходимый драйвер не загружается через использование системного свойства “jdbc.drivers”.
Cloudscape может работать в двух режимах:
- внедренном, когда СУБД загружается по требованию прикладной программы,
а по окончанию работы приложения СУБД выгружаетс. В этом случае необходимо загружать драйвер
“com.ibm.db2j.jdbc.DB2jDriver” с помощью следующей инструкции
Class.forName(“com.ibm.db2j.jdbc.DB2jDriver ”).newInstance();
- клиент-серверном, в этом режиме серверная часть СУБД загружается и должная
функционировать до активации клиента, который загружает только клиентскую версию драйвера
“ COM.cloudscape.core.RmiJdbcDriver” инструкция загрузки драйвера практически идентична предыдущей
Class.forName(“COM.cloudscape.core.RmiJdbcDriver”).newInstance().
После загрузки драйвера разработчик обращается к менеджеру драйверов с просьбой установить соединение с конкретным источником данных, передавая ему в качестве параметра URL в следующей форме “jdbc:db2j:Имя Вашей БД”. И тут начинаются хитрости.
Как узнать где физически располагается БД и в каком виде (единственный файл, в котором хранятся все объекты БД, подобно тому, как хранится БД msaccess, а может отдельный объект БД хранится в независимом файле). В терминологии cloudscape важное место занимает понятие «системной директории» в которой размещаются все каталоги баз данных, файлы настроек, файлы log. Настройки хранятся в файле “db2j.properties”, опции указанные в этом файле называются “system-wide”, что в отличие от “database-wide” свойств указывают на глобальный характер их действия т.е. на все БД в данной системной директории, если, конечно, данные свойства не были переопределены на уровне конкретной БД. Системная директория не имеет конкретного местоположения. Так каждый раз при запуске RDBMS Cloudscape вам необходимо указывать ее местоположение, т.е. возможно существование нескольких системных директорий, каждую из которых должен будет обслуживать отдельный экземпляр Cloudscape. Запускать несколько экземпляров Cloudscape работающих с одной системной директорией опасно из-за возможных повреждений БД, нарушения их целостности. Если же при запуске Cloudscape вы не указали местоположение системной директории, то в качестве оной принимается текущая директория. Следовательно, URL “jdbc:db2j:mybase” может указывать на две различные БД, если приложение будет запускаться из различных мест.
Подобная неопределенность имеет как ни странно и положительные моменты. Стадия загрузки БД требует достаточно длительного времени, а если БД была повреждена и идет попытка ее восстановления, то и подавно. Следовательно, хорошим решением при работе Cloudscape в клиент-серверном режиме является использование системного свойства “db2j.system.bootAll”, которое приводит к автоматической загрузке всех БД расположенных в системной директории. Cloudscape по умолчанию загружает БД при первой попытке обращения к ней. Cloudscape как хорошая RDBMS контролирует действия разработчика предотвращая ошибку во внедренном режиме работать с одной физической БД двум экземплярам Cloudscape, размещая в внутри системной директории файл блокировки “db.lck”. Поэтому попытка повторной загрузки БД приводит к генерации исключения. На поведение Cloudscape при двойной загрузке влияет системное свойство “db2j.database.forceDatabaseLock”.
После окончания работы с БД вы можете дать команду на выгрузку или конкретной БД с помощью следующего URL “jdbc:db2j:Имя БД которую надо выгрузить;shutdown=true”, а если надо выгрузить Cloudscape то “jdbc:db2j:;shutdown=true” с помощью следующей строки кода DriverManager.getConnection (“jdbc:db2j:;shutdown=true”). Разумеется, что дать команду на выгрузку БД вы можете дать только во внедренном режиме.
Физически БД Cloudscape представляет собой директорию в которой находятся файлы и каталоги соответсвующие объектам БД, файлы настроек с помощью которых можно управлять набором свойств БД.
Каталог БД состоит из следующих объектов:
- каталог “log” в котором размещается журнал действий над БД и
используется для восстановления БД в случае ошибок;
- каталог “seg0” в котором размещаются файлы соответствующие таблицам и индексам БД;
- файл “service.properties” содержит конфигурационную информацию БД,
и как в нем самом написано
“ Please do NOT edit this file. CHANGING THE CONTENT OF THIS FILE MAY CAUSE DATA CORRUPTION.”;
- каталог “tmp” – временная директория используемая Cloudscape
при выполнении ресурсоемких операций над БД;
- каталог “jar” служит для хранения архивов с классами java хранимыми в БД;
- файл “db.lck” – признак блокировки БД.
Cloudscape предоставляет очень интересную возможность, если вы используете БД только для выборок данных без изменений, то можно сжать БД поместив ее в архив jar или zip для последующего использования.
Для создания БД при первом обращении необходимо указать атрибут “create=true” например следующая команда создает БД “myBase”
DriverManager.getConnection ("jdbc:db2j:myBase;create=true")
Если необходимо обратиться к БД находящейся за пределами системной директории, то можно указать либо абсолютный путь к БД или относительный, предполагая что текущая директория это системная. Пример URL “jdbc:db2j:myBase”, “jdbc:db2j:с:\\myDir\\myBase”. Очень важно понимать, что как только вы соединяетесь с БД пусть даже физически и нерасположенной в системной директории, то логически она снова становится частью системного каталога, т.е. на ее распространяются “system-wide” свойства системной директории.
Cloudscape интегрируется с java на очень высоком уровне и следовательно, одна из важнейших настроек jvm “classpath” также рассматривается как возможное местоположение БД. Таким образом, если есть “classpath=c:\mydir;и другие каталоги” а в директории “c:\mydir” находится база “myBase”, то использовании соединения с использованием URL “jdbc:db2j:myBase” физически будет использоваться каталог БД “c:\mydir\mybase”. Возможна ситуация когда в системной директории также находится БД “myBase”, тогда возникшую неоднозначность можно разрешить с использованием дополнительного префикса “directory”, в этом случае URL соединения с БД будет выглядеть следующим образом “jdbc:db2j:directory:myBase”. Если БД размещается внутри архива, то следует использовать следующий URL “jdbc:db2j:jar(c:\\mydir\myarchi.jar)mybase”. При установлении соединения с БД указание местоположения каталога является необходимым, но вовсе не единственной компонентой URL. Полный список атрибутов можно найти в документации поставляемой вместе с Cloudscape, однако минимально необходимыми являются следующие атрибуты:
- “create=true” – создать БД если ее не существует;
- “bootPassword=пароль” – существует возможность защитить БД
используя шифрование данных с использованием данного пароля;
- для локализации Cloudscape следует использовать свойство “locale=ссылка на локальные настройки”;
- “user=имя пользователя” - имя пользователя соединеющегося с БД,
используется при включении системы аутентификации.
- “password=пароль” – пароль пользователя;
- “shutdown=true” – завершить сеанс работы с БД;
- “dataEncryption=true” – при создании БД можно указать, что данные
хранящиеся в ней должны шифроваться.
Для того чтобы URL соединения с БД не разрастался можно вынести все эти дополнительные атрибуты в объект “java.util.Properties” который обладает очень приятной возможностью сохранять и восстанавливать свое состояние в файле.
Примерный код модуля сохранения настроек:
Properties props = new Properties ();
props.setProperty("Первый атрибут", getValueOfFirstAttribute());
props.setProperty("Второй атрибут", getValueOfSecondAttribute());
props.store(new FileOutputStream("mysettings.txt") , "Cloudscace Attributes");
//И соответственно парный ему код чтения настроек из файла и использование их при установлении соединения с БД.
Properies props2 = new Properties ();
props2.load(new FileInputStream("mysettings.txt"));
Connection con = DriverManager.getConnection("jdbc:db2j:myBase" , props);
Тесная интеграция java и Cloudscape позволяет хранить в БД не только информацию тип которой соответствует стандарту SQL-92, но и произвольные объекты java, на которые накладывается только одно ограничение это способность к сериализации. Важнейшим требованием предъявляемым с СУБД – это эффективность средств обеспечивающих ссылочную целостность БД, когда все ссылки на внешние объекты в каждом объекте БД должны быть действительными, что в свою очередь является важным условием для обеспечения точности и полноты данных. Если условие ссылочной целостности не выполняется, то следствием будет потеря данных , неточные, а то и противоречивые сведения, напрасные затраты памяти и вычислительных ресурсов. В приведенном ниже примере заданы две таблицы “Students” и “Groups”.
# студента | # группы | ФИО |
1 | 12345 | Vasya |
2 | 23456 | Petya |
3 | 34567 | Anatoli |
# группы | Курс | Факультет |
12345 | 2 | Abc |
23456 | 3 | Cde |
В таблице “Students” присутствует запись студента Anatoli который учится в группе 34567. однако запись о данной группе отсутствует в таблице “Groups”, следовательно перед нами запись-фантом, которая вроде бы есть, а может и нет.
СУБД высокого уровня имеют различные средства для обеспечения целостности, это и ограничения на значения отдельных столбцов, ограничения на уровне таблицы, триггеры. Для автоматизации выполнения больших и сложных последовательностей действий используются хранимые процедуры. Поддержка транзакций, блокировка записей и таблиц также признак хорошей СУБД.
Первейшим и наиболее распространенным средством обеспечения целостности является объявление полей таблицы как “primary key”. При создании ключа автоматически создается индекс используемый для оптимизации упорядочения записей таблицы. Ведь вы на самом деле не думаете, что когда вы выполняете сортировку таблицы из миллиона или двух записей, то СУБД физически переупорядочивает все эти записи.
CREATE TABLE USERS ( USERID INT CONSTRAINT pk PRIMARY KEY NOT NULL DEFAULT AUTOINCREMENT, LAST_NAME CHAR (25), FIRST_NAME CHAR(25), BIRTH_DAY DATE, DEPARTMENTID INT )
Сразу хочется предупредить, что раз данный обзор идет применительно БД Cloudscape, то хотя общие моменты являются универсальными и теоретическими, то на этапе практической реализации можно столкнуться с особенностями конкретной СУБД. Так для СУБД mysql приведенный выше синтаксис не корректен, там для указания того что столбец USERID должен иметь тип AUTOINCREMENT следует использовать “userid int auto_increment primary key”. Столбец “USERID” с автоматически генерируемыми значениями необходим для однозначной идентификации записей в таблице т.к. никакие другие поля или их комбинации не дают подобной уверенности. Вполне возможна ситуация когда два или более сотрудника будут иметь одинаковые имена и фамилии, а также дни рождения (парадокс дня рождения из теории вероятности). Такого же результата можно добиться используя ограничение UNIQUE. Ограничениям накладываемым на поля можно давать имена, например, в следующем примере созданы ограничения с именами.
CREATE TABLE POST ( POSTID INT CONSTRAINT pk PRIMARY KEY, POSTNAME CHAR(50) CONSTRAINT pn NOT NULL, CITY INT CONSTRAINT ct UNIQUE )
ограничение UNIQUE не предотвращает вставку NULL значений поэтому имеет смысл использовать данное ограничение для полей которые должны быть уникальными, но не обязательно должны быть заполнены. Например, информация о сотрудниках имеющих медицинскую страховку. Очевидно, что два сотрудника не могут иметь одинаковые номера медицинских страховок, однако возможна ситуация когда у сотрудника нет медицинской страховки. Для того чтобы указать, что поле не может иметь неопределенного значения используют ограничение NOT NULL.
Следующей стадией обеспечения целостности является использование ограничений CHECK. Пример в котором на поле MONEY накладывается ограничение значения только диапазоном положительных значений.
CREATE TABLE USERS( USERID INT PRIMARY KEY, FIO CHAR(50), MONEY DOUBLE PRECISION CHECK ( MONEY >= 0) )
Ограничения CHECK предоставляют средство контроля за значениями полей таблицы, однако если необходимо определять возможный диапазон значений столбцы на основе сложного вычисления, включающего выборки данных из нескольких таблиц, то в этом случае следует пользоваться триггерами. Разработчик java всегда должен помнить соответствие между типами java и типами SQL. В Cloudscape интеграция между этими типами данными доведена до такой степени, что возможна следующая строка кода.
SELECT FIO FROM USERS WHERE DATE_BIRTH.before ( new java.util.Date (10 , 11 , 102) )
Где поле DATE_BIRTH имеет тип “java.util.Date”. Возможен также и следующий фрагмент.
SELECT FIO.substring(0 , FIO.indexOf(‘ ’)) AS ONLY_NAME FROM USERS;
Преобразование между типами java и SQL выполняется максимально прозрачно для разработчика. Конечно некоторое неудобство составляет необходимость использования полных имен классов, но и в это случае можно сэкономить время создав псевдоним для класса.
CREATE CLASS ALIAS SimpleDate FOR java.util.Date
Следующим шагом будет возможность создания полей таблиц в которых могут храниться объекты java.
CREATE TABLE USERS ( USERID INT PRIMARY KEY, FIO java.lang.String, DATE_BIRTH java.util.Date, FOTO mypackage.Foto PASSPORT mypackage.social.Passport )
Как я отмечал ранее нет никаких ограничений на тип хранимых данных в таблицах за исключением того что тип данных должен обладать способностью сохранять свое состояние в потоке в виде набора байт и восстанавливать его оттуда. Такая способность называется сериализация. В любой уважающей себя книжке по java легко можно найти подробное руководство по работе с этой технологией. Основные моменты на которые следует обращать внимание при реализации способности объектов сохранять свое состояние между сеансами работы jvm, а это по сути и есть технология сериализации, сводятся к поддержке одного из двух интерфейсов. Это или java.io.Serializable или же java.io.Externalizable. Первый способ наиболее прост так для того чтобы объекты класса ABC получили возможность храниться в БД достаточно чтобы быть сохраняемый объект реализовывал интерфейс Serializable или наследовал его реализацию из его иерархии классов. И больше ничего. Не требуется ни единой строчки кода. Пример приведен ниже:
package mp;
import java.io.*;
public class User implements Serializable {
public String userFIO;
public int userID;
public boolean equals(Object ob) {
if (!(ob instanceof User))
return false;
User u = (User) ob;
return u.userFIO.equals(this.userFIO) && u.userID == this.userID;
}
}
Для эксперимента можно сохранить объект в файл а потом его восстановить и проверить, что восстановленный объект будет идентичен исходному.
package mp;
import java.io.*;
public class SaveAndRead {
public static void main(String[] args) throws Exception
{
FileOutputStream fout = new FileOutputStream ("boo.ser");
ObjectOutputStream oout = new ObjectOutputStream (fout);
User c1 = new User ();
c1.userFIO = "Vasya";
c1.userID = 12345;
oout.writeObject(c1);
oout.flush();
oout.close();
FileInputStream fin = new FileInputStream ("boo.ser");
ObjectInputStream oin = new ObjectInputStream (fin);
Object c2 = oin.readObject();
System.out.println("c1 == c2 : " + c2.equals(c1) );
}
}
Результат вывода на экран: “ c1 == c2 : true”, следовательно объект был сохранен и восстановлен без потерь. Важным моментом является то что в файле “boo.ser” было сохранено только состояние объекта и ничего более, определение класса User должно быть доступно для той стороны, которая восстанавливает объект. Пока все очень просто. Но если начать строить более сложные иерархии классов, то можно столкнуться с рядом вопросов. Например, существует класс User2 который наследуется от класса User, и должен автоматически наследовать способность к сериализации. Однако, если мы хотим запретить сохранение объектов типа User2 в потоке, то следует перекрыть два метода, которые неявно добавляются компилятором к определению класса и реализуют саму логику сохранения объекта в потоке или восстановления из потока.
private void writeObject(ObjectOutputStream out) throws IOException{
throw new NotSerializableException ("class User2 cannot be serialized");
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
throw new NotSerializableException ("class User2 cannot be serialized");
}
более деликатным способом отказа от сериализации объектов может служить модификатор полей класса “transient”. Использование данного модификатора означает что поле не является устойчивой частью объекта и не может быть сохранено. Например попытка запуска следующего фрагмента кода приведет к генерации исключения.
package mp;
public class User2 extends User
{
Thread th = new Thread (){
public void run (){
int i = 0;
while (i++ < 1000) {
System.out.println("Hello my name is " + userFIO == null ? "unknown": userFIO );
try{
sleep (500);
}
catch (Exception exc){System.out.println ("Error " + exc);}
}
}
};
public User2 (){
super (null , 0);
th.start();
}
public User2 (String userName , int userID){
super (userName , userID);
th.start ();
}
}
При попытке сохранить в поток объект потока будет выброшено исключение “ java.io.NotSerializableException: mp.User2$1”.
Другого было бы невозможно и ожидать. Вместо того чтобы создавать поле типа Thread можно было бы обойтись любым другим классом, который не имеет поддержки интерфейса Serializable, но мне хотелось просто показать здесь абсурдность всякой попытки слепо сериализовать любые объекты. Необходимо понимать, что многие классы имеют такую природу, что имеют смысл в контексте только текущего сеанса работы jvm. В таком случае поля следует объявлять как transient.
Использование интерфейса Serializable является наиболее простым и соответственно наиболее грубым средством сохранения объектов в потоке. Большую гибкость может дать использование интерфейса Externalizable. В данном случае программист обязан реализовать саму логику сохранения объекта в потоке, например, можно хранить объекты в потоке используя шифрование, причем избирательно для некоторых полей. Однако на этом пути есть несколько подводных камней. Прежде всего необходимо сказать что классы ObjectInputStream и ObjectOutputStream выполняют кэширование объектов сохраняемых в потоке с целью недопущения следующей ситуации.
Предположим что существует ресурс A, который может существовать только в одном экземпляре, но допускает одновременное использование себя двумя или более клиентами, также есть класс CA использующий данный ресурс. Если существует два объекта класса CA в каждом из которых есть ссылка на один экземпляр данного объекта. Простейшая попытка реализации записи этих двух объектов в поток не учитывая общего поля приводит к сохранению в потоке двух копий нашего не допускающего дублирования ресурса. Следовательно существует необходимость учитывать историю записи объектов в поток и при повторной записи сохранять не сам объект а только ссылку на него. Соответственно должен будет себя вести и код по восстановлению объектов. А вот и первый подводный камень:
public class SaveAndRead
{
public static void main(String[] args) throws Exception{
FileOutputStream fout = new FileOutputStream ("boo.ser");
ObjectOutputStream oout = new ObjectOutputStream (fout);
User c1 = new User2 ("Vasya" , 12345);
oout.writeObject(c1);
c1.userFIO = "Petya";
oout.writeObject(c1);
oout.flush();
oout.close();
FileInputStream fin = new FileInputStream ("boo.ser");
ObjectInputStream oin = new ObjectInputStream (fin);
User c2 = (User2)oin.readObject();
User c21 = (User2)oin.readObject();
System.out.println("c2 == c21 : " + c21.equals(c2) );
}
Удивительное дело, но объект c2 равен объекту с21 хотя имя первого сохраненного объекта “vasya” а второго – “petya”. Дело в том что стандартный механизм сохранения объектов в потоке учел что мы уже сохраняли объект c1 и при повторной записи не отследил изменение его состояния, и записал повторно ссылку на первый объект. Выходом из ситуации будет использование определенного в классе ObjectOutputStream метода reset, который очищает кэш объектов, но согласитесь, что это слишком грубое средство. Поэтому в ряде ситуаций лучше бывает затратить несколько больше времени на создание собственной реализации сохранения объектов в потоке, чем потом удивляться столь не понятным результатам. Интерфейс Externalizable содержит два метода:
public void writeExternal(ObjectOutput out) throws IOException;
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
Переопределите эти методы для предоставления вашего протокола. Несмотря на то, что это более сложный сценарий, он также наиболее контролируемый.
Существует также и третий способ дать возможность сохранять объекты java в таблицах cloudscape. По сути и даже технически он похож на способ с “Externalizable” и заключается в поддержке интерфейса java.sql.SQLData и соотвественно определенных в данном интерфейсе методов. Ниже приводится пример с использованием данного интерфейса.
package mp;
public class CATEGORIES{
public static final int COUNT_CATEGORIES = 3;
public static final int FANTASY = 1;
public static final int SCIENCE = 2;
public static final int ANIMALS = 3;
/**
* а также описание еще нескольких десятков констант соответсвующих
* категориям книг
*/
}
// /=======================================================
package mp;
import java.sql.*;
public class myBook implements SQLData{
public String title;
public int authorID;
public int pages;
public int categoryID;
public String getSQLTypeName() throws SQLException{
return "myBook";
}
public void readSQL(SQLInput stream, String typeName) throws SQLException{
title = stream.readString();
authorID = stream.readInt();
pages = stream.readInt();
categoryID = stream.readInt();
}
public void writeSQL(SQLOutput stream) throws SQLException{
stream.writeString(title );
stream.writeInt(authorID);
stream.writeInt(pages);
stream.writeInt(categoryID);
}
}
и наконец пример методов создающих таблицу для хранения объектов класса “myBook” в базе и делающих выборки данных из таблицы записей.
public void someTestWithBooks() throws Exception{
ResultSet rs = co.getConnection().
createStatement().executeQuery (
"select * from books where book->categoryID = mp.CATEGORIES::FANTASY");
while (rs.next())
System.out.println(rs.getObject(1) + "; " + rs.getObject(2) );
rs.close ();
}
Продолжение здесь.