JDBC
Эта глава посвящена JDBC (Java DataBase Connectivity) — набору классов и методов, используемых в языке программирования Java для работы с базами данных. JDBC обеспечивает простые, универсальные и хорошо адаптируемые средства взаимодействия с различными СУБД. Материал этой главы требует некоторого опыта программирования на Java.
Интерфейсы JDBC, разработанные корпорацией Sun, обеспечивают выполнение всех стандартных операций с базами данных SQL, а разработчики PostgreSQL предоставляют конкретную реализацию этих интерфейсов. Реализация производит все взаимодействие с базой данных: подключение, регистрацию, вызов хранимых процедур и т. д. Интерфейсы спроектированы таким образом, что программа, использующая JDBC, может подключиться к любой JDBC-совместимой базе данных без модификации кода. Впрочем, при этом все же необходимо учитывать некоторые обстоятельства.
Во-первых, JDBC не выполняет лексического анализа или проверки синтаксиса SQL на стороне клиента. Команды просто передаются базе данных независимо от их правильности. Таким образом, если код SQL работает в одной СУБД, но не подходит для другой, реализация не будет знать об этом до момента фактической установки соединения и пересылки кода SQL. В настоящее время Sim пытается решить эту проблему. Возможно, соответствующие изменения будут внесены в следующие версии JDBC или с переходом на другой стандарт.
Во-вторых, в реализацию включаются дополнительные классы, специфические для конкретного продукта. Например, в PostgreSQL имеются расширения для геометрических типов данных. Они существуют только в PostgreSQL и не поддерживаются другими фирмами. Если вы используете эти специализированные классы, программа не будет работать в других JDBC-совместимых базах данных, невзирая на следование «стащшрту» JDBC.
Одно из преимуществ драйвера JDBC для PostgreSQL заключается в том, что он является драйвером «четвертого типа». Это означает, что он написан на «чистом» языке Java, что позволяет перенести его куда угодно и использовать на любой платформе с поддержкой TCP/IP, поскольку драйвер подключается только через TCP/IP.
Построение драйвера JDBC для PostgreSQL
В этом разделе предполагается, что СУБД PostgreSQL уже установлена в вашей системе и подготовлена к работе. Проследите за тем, чтобы она была готова к приему входящих подключений TCP/IP — это можно сделать при запуске процесса postmaster. За информацией о ключах запуска postmaster обращайтесь к главе 9.
Прежде чем использовать JDBC, следует построить драйверы JDBC для PostgreSQL. Для этого необходимо иметь исходные тексты драйверов на языке Java, входящие в полную поставку PostgreSQL и в пакет opt. Их также можно загрузить с сайта PostgreSQL; за информацией о загрузке и установке обращайтесь к главе 2.
Кроме того, вам понадобится Ant — стандартная система построения продуктов Java, отдаленный аналог утилиты gmake из проекта Apache Jakarta. Пакет Ant необходим для построения драйвера JDBC для PostgreSQL. Дополнительную информацию об Ant можно получить по адресу http://jakarta.apache.org/ant/index.html. Проследите за тем, чтобы каталог bin пакета Ant был указан в пути поиска.
Если поддержка Java не была включена при исходной сборке PostgreSQL, перейдите на верхний уровень дерева исходных текстов PostgreSQL и введите команду ./configure with-Java с остальными параметрами, использовавшимися в исходной конфигурации. Команда генерирует make-файлы заново и при необходимости добавляет поддержку Java.
Далее происходит сборка драйвера и реализации. Перейдите в каталог src/ interfaces/jdbc и введите команду gmake. Команда строит два файла с расширением jar: файл postgresql.jar содержит класс Dri ver и другие конкретные реализации, а файл postgresql-examples.jar содержит откомпилированные классы примеров.
Использование драйвера PostgreSQL
В этом разделе рассматривается использование встроенного драйвера JDBC PostgreSQL. Начните с включения пути к файлу postgresql.jar в переменную CLASSPATH. Для этого можно либо задать значение переменной среды CLASSPATH, либо передавать путь в командной строке исполняемого файла при каждом запуске приложения Java. Дополнительную информацию о настройке пути к классам можно найти в инструкциях разработчика вашей виртуальной машины Java.
Далее в процессе написания приложения Java необходимо обеспечить регистрацию класса Driver в программе. При прохождении через загрузчик классов Java класс Driver автоматически регистрируется в классе DriverManager; по этим данным JDBC узнает, какой вариант класса Driver должен использоваться при подключении к базе данных определенного типа. Например, при подключении к базе данных PostgreSQL должен использоваться класс драйвера PostgreSQL и т. д.
Чтобы класс Dri ver был зарегистрирован загрузчиком классов, можно выполнить поиск по имени, как в небольшом фрагменте Java-программы, приведенном в листинге 12.1.
Листинг 12.1. Поиск по имени класса
try {
Class.forName("org.postgresql.Dri ver"):
} catch (ClassNotFoundException cnfe) {
System.err.println("Couldn't find driver class:"):
cnfe.printStackTrace():
}
Метод Cl ass. forName выполняет поиск класса по имени. В данном примере для поиска используется имя «Driver». Загрузчик просматривает содержимое CLASSPATH и ищет класс с заданным именем. Если поиск оказывается успешным, загрузчик читает двоичное описание класса. Если класс не найден, инициируется исключение ClassNotFoundException; в нашем примере обработчик этого исключения выводит сообщение об ошибке. Если это произошло, значит, либо была допущена ошибка при построении драйвера, либо файл .jar отсутствует в пути CLASSPATH.
После регистрации класса Driver наступает следующая стадия — подключение к базе данных PostgreSQL с использованием класса DriverManager. Класс Dri verManager отвечает за работу с URL-адресами JDBC, выбор подходящего драйвера и его дальнейшее применение для подключения к базе данных. URL-адреса JDBC состоят из трех частей, разделенных двоеточиями:
jdbc: тил_дрдйвера : база_двнных
Первая часть, jdbc, всегда остается постоянной и указывает на то, что подключение производится к источнику данных JDBC. Вторая часть, mun_dpaueepa, представляет тип базы данных, к которой вы хотите подключиться. Для подключения к базе данных PostgreSQL используется строка postgresql. Третья часть передается драйверу и используется им для поиска базы данных. Она имеет один из трех форматов:
база_данных
//хост/база_данных
//хост:порт/бвза_данных
В первом формате база данных PostgreSQL работает на локальном хосте со стандартным номером порта, а параметр база_даиных определяет имя базы данных для подключения. Во втором формате задается имя хоста и имя базы данных со стандартным номером порта. Третий формат позволяет задать номер порта. Даже при использовании URL-адреса первого типа подключение JDBC всегда производится через TCP/IP.
Во всех примерах этой главы будет использоваться URL-адрес jdbc:postgresql:// localhost/booktown — мы подключаемся к базе данных booktown на хосте local host. Давайте попробуем объединить все, о чем говорилось в этой главе, и подключиться к базе данных. В листинге 12.2 приведена простая программа Java, открывающая подключение JDBC к базе данных booktown.
Листинг 12.2. Простое подключение JDBC
import Java.sql.DriverManager:
import Java.sql.Connection:
import Java.sql.SQLException;
public class Examplel {
public static void main(String[] argv) {
System.out.printlnC'Checking if Driver is registered with DriverManager."):
try {
Class.forNaraeC'org.postgresql .Driver"):
} catch (ClassNotFoundException cnfe) {
System. out. printlnt "Couldn't find the driver!");
System. out. println( "Let's print a stack trace, and exit."):
cnfe . pri ntStackTrace( ) ;
System.exit(l) ; }
System. out. pri ntln( "Registered the driver ok. so let's make a connection."):
Connection с = null :
try {
// Во втором и третьем аргументах передаются соответственно
// имя пользователя и пароль. Замените данными пользователя
// в своей системе.
с = DriverManager.getConnection("jdbc:postgresql ://localhost/booktown".
"username". "password");
} catch (SQLException se) {
System. out. printlnt "Couldn't connect: print out a stack trace and exit."):
se.printStackTrace() ;
System.exit(l): }
if (c != null)
System. out. printlnC'Hooray! We connected to the database!");
else .
System. out. printlnC'We should never get here.");
}
}
Далее при помощи объекта Connection выполняются любые операции с базой данных PostgreSQL
Обратите внимание на первые три строки листинга 12.2. В них три команды import импортируют классы, необходимые для регистрации драйвера в объекте DriverManager, а также для создания объектов Connection и SQLException. В общем случае команда импортирования, обеспечивающая доступ к классам JDBC, имеет следующий синтаксис (класс — имя класса, экземпляр которого вы хотите создать):
import java.sql .класс
Если вы не уверены в том, какие именно классы вам нужны, импортируйте все классы JDBC следующей командой:
import java.sql .*
Учтите, что импортирование всех классов JDBC означает большое количество непроизводительных затрат. Чтобы программа работала с максимальной эффективностью, следует ограничиваться только теми классами, которые действительно необходимы для работы приложения.
Использование JDBC
В этом разделе рассматриваются основные принципы использования JDBC, некоторые технические аспекты, потенциальные проблемы и т. д. За дополнительными сведениями обращайтесь на сайт JDBC (http://java.sun.com/products/jdbc/), на котором всегда приводится самая свежая информация и имеются ссылки на множество полезных ресурсов. Подробные описания конкретных классов, методов и полей приведены в документации API, входящей в комплект поставки JDK. Обращайтесь к пакету java.sql.
Классы JDBC представляют основные компоненты взаимодействия программы с SQL. У всех основных классов JDBC — Connection, Statement, ResultSet, Blob и Clob — имеются прямые аналоги в SQL. Кроме того, в JDBC включены вспомогательные классы — например, классы ResultsSetMetaData и DatabaseMetaData предназначены для работы с метаданными. В частности, они используются для получения информации о возможностях базы данных, для проверки типа результата запроса, в процессе отладки и просто в ситуациях, когда вы не располагаете информацией о данных, с которыми работаете.
Интерфейс JDBC в PostgreSQL также содержит классы для работы с нестандартными расширениями PostgreSQL. К их числу относятся Fastpath, геометрические типы, большие объекты и классы, упрощающие сериализацию объектов Java в базе данных.
Принципы использования JDBC
В листинге 12.2 приведен пример использования объекта Connection, представляющего физическое подключение к базе данных. Объект Connection требуется для создания объектов Statement, при помощи которых в JDBC базе данных передаются команды SQL.
Существует три разновидности объектов Statement: базовый класс Statement, классы PreparedStatement и CallableStatement.
Объект Statement создается методом createStatement (листинг 12.3).
Листинг 12.3. Создание объекта Statement
Statement s = c.createStatement():
В листинге 12.3 создается объект класса Statement с именем s для объекта Connecti on с именем с. Далее созданный объект Statement может использоваться для выполнения запросов и обновлений базы данных.
В классе Statement особенно важны два метода. Первый, executeQuery, получает один аргумент (код выполняемой команды SQL) и возвращает объект класса ResultSet, о котором речь пойдет ниже. Метод executeQuery предназначен для выполнения команд, возвращающих наборы данных, например запросов SELECT. Возвращаемый объект ResultSet представляет данные, полученные в ходе запроса.
Пример выборки данных из базы booktown приведен в листинге 12.4.
Листинг 12.4. Простая выборка в JDBC
Statement s = nul 1: try
{
s = c.createStatementO;
} catch (SQLException se) {
System.out.printlnC'We got an exception while creating a statement:" +
"that probably means we're no longer connected."):
se.printStackTrace();
System, exit(l);
}
ResultSet rs = null:
try {
rs = s.executeQuery("SELECT * FROM books");
} catch (SQLException se)
{
System.out.printlnC'We got an exception while executing our query:" +
"that probably means our SQL is invalid"):
se.pnntStackTrace():
System.exit(l):
}
int index = 0:
try { while (rs.nextO)
{
System.out.printlnC'Here's the result of row " + index++ + ":"):
System.out.pri ntln(rs.getStri ng(1)):
}
} catch (SQLException se) {
System.out.pnntlnC'We got an exception while getting a result:this " +
"shouldn't happen: we've done something really bad.");
se.pnntStackTrace();
System.exit(l):
}
Сначала мы создаем объект Statement, а затем используем метод executeQuery этого объекта для выполнения запроса SELECT * FROM books. Возвращенный запросом объект ResultSet используется для вывода полученной информации.
Объект Resul tSet предоставляет основной интерфейс выборки из базы данных. Он обладает двумя основными возможностями: возможностью последовательного перебора полученных записей и возможностью возвращения значения заданного поля текущей записи. Принцип перебора такой же, как в стандартных перечислениях Java: вы начинаете перебор в позиции перед первым элементом и последовательно переходите к следующему элементу методом next.
Метод next возвращает true в том случае, если объект ResultSet успешно перешел к следующей записи (то есть в итоговом наборе еще остались необработанные записи). Цикл whi I e в листинге 12.4 выводит значение первого поля каждой из возвращаемых записей. Если итоговый набор не содержит ни одной записи, первый же вызов next вернет fal se и программа ничего не выведет.
Класс ResultSet может возвращать значения различных типов; в листинге 12.4 первое поле интерпретируется как строка (Stri ng). К счастью, все стандартные типы данных SQL могут быть представлены в строковом виде, поэтому независимо от типа данных вы всегда сможете получить значение первого поля и вывести его. Класс ResultSet содержит множество других методов, включая методы выборки для всех типов данных SQL и преобразования их к типам Java. За дополнительной информацией обращайтесь к описанию ResultSet в документации API.
Другой важный метод, executeUpdate, тоже вызывается с одним аргументом — выполняемой командой SQL Различие между executeQuery и executeUpdate состоит в том, что метод executeUpdate предназначен для выполнения команд, изменяющих состояние данных в базе. Например, при вызове executeUpdate для команды CREATE, INSERT или UPDATE возвращается число типа int, определяющее количество модифицированных записей.
В листинге 12.5 метод executeUpdate используется для вставки новой записи в таблицу books.
Листинг 12.5. Простая вставка в JDBC
Statement s - null: try {
s = c.createStatement():
} catch (SQLException se) {
System.out.printlnC'We got an exception while creating a statement:" +
"that probably means we're no longer connected."):
se.printStackTrace();
System.exlt(1):
}
int m = 0;
try {
m = s.executeUpdateC1 INSERT INTO books VALUES " +
"(41472. 'Practical PostgreSGl'. 1212. 4)"): >
} catch (SQLException se) {
System.out.println("We got an exception while executing our query:" +
"that probably means our SQL is invalid"): >
se.printStackTrace():
System.exit(l):
}
System.out.println("Successfully modified " + m + " rows.\n"):
Нетривиальные возможности JDBC
Как упоминалось выше , кроме базового объекта Statement в JDBC существует два других типа объектов для представления команд: PreparedStatement и Cal lableStatement. Они будут описаны ниже.
В данном подразделе также будет рассказано об объектах ResultsSetMetaData и DatabaseMetaData. С их помощью можно запросить у JDBC описание результатов запроса или базы данных. Возможность получения такой информации на стадии выполнения программы позволяет динамически выполнять команды SQL — даже такие, параметры которых были неизвестны на момент написания программы.
Объект CallableStatement
Объект CallableStatement позволяет выполнять хранимые процедуры в JDBC-co-вместимых базах данных. Лучшим источником информации по этому вопросу является сайт Sun Javasoft (http://java.sun.com/products/jdbc/), поскольку стандарт вызываемых команд (callable statements) изменяется и развивается и его практические применения зависят от версии Java и JDBC.
Объект PreparedStatement
Объект PreparedStatement представляет подготовленные (prepared) команды SQL, многократно выполняемые с разными исходными данными — например, если вам потребовалось вставить в таблицу несколько записей, одну за другой. Главное преимущество PreparedStatement заключается в том, что команда проходит предварительную компиляцию, что избавляет от затрат на повторную обработку команд SQL при каждом выполнении. Пример использования объекта PreparedStatement приведен в листинге 12.6.
Листинг 12.6. Использование подготовленных команд в JDBC
PreparedStatement ps = null:
try {
ps = c.prepareStatementC'INSERT INTO authors VALUES (?. ?. ?)");
ps.setlntd. 495):
ps.setString(2. "Light-Williams"):
ps.setStringO. "Corwin"): } catch (SQLException se) {
System.out.printlnC"We got an exception while preparing a statement:" +
"Probably bad SQL."):
se.printStackTrace();
System.exit(l);
}
try {
ps.executeUpdate():
} catch (SQLException se) {
System.out.printlnC'We got an exception while executing an update:" +
"possibly bad SQL. or check the connection."):
se.pnntStackTrace():
System.exit(l):
}
Как видно из листинга, подготовленная команда выглядит вполне привычно, разве что все переменные величины заменяются в ней вопросительными знаками (?). Присваивание выполняется методами класса PreparedStatement (setlnt, setString и т. д.). Выбор метода для каждого поля зависит от типа данных этого поля.
Объекты PreparedStatement удобны тем, что они обеспечивают автоматическое преобразование типов данных Java в типы SQL. Например, при переходе к типу text вам не нужно беспокоиться об экранировании символов или кавычках.
Обратите внимание: первый аргумент метода set идентифицирует номер позиции переменной (вопросительного знака), которой присваивается значение. Единица означает первый вопросительный знак, двойка — второй и т. д.
Другая сильная сторона PreparedStatement связана с тем, что объект можно снова и снова использовать с новыми данными, не создавая нового объекта Statement для каждого набора параметров. Конечно, такой подход более эффективен, поскольку он ограничивается созданием одного объекта, а новые значения переменных задаются методами set.
ResultSetMetaData
У JDBC можно запросить подробную информацию об итоговом наборе запроса. Класс Resul tsSetMetaData возвращает описание объекта Resul tSet, полученного при вызове executeQuery. В нем содержится информация о количестве полей, типе данных, именах полей и т. д.
Из всех методов класса ResultSetMetaData чаще всего используются методы getCol umnName и getCol umnTypeName. Они возвращают соответственно имя поля и имя его типа данных в виде значения типа String.
ПРИМЕЧАНИЕ
Не путайте метод getCol umnType с методом getCol umnTypeName. Метод getCol umnType возвращает значение типа int, соответствующее внутреннему идентификатору типа данных в JDBC, тогда как getCol umnTypeName возвращает имя типа в формате String.
Использование JDBC 363
В листинге 12.7 класс ResultSetMetaData используется для получения имени и типа данных первого поля объекта ResultSet с именем rs. Программа является логическим продолжением листинга 12.4, в котором был создан объект rs.
Даже если отвлечься от соображений эффективности, механизм PreparedStatement гораздо надежнее подготовки нескольких команд в объектах Statement.
Листинг 12.7. Использование объекта ResultSetMetaData
ResultSetMetaData rsmd - null:
try {
rsmd = rs.getMetaData():
} catch (SQLException se) {
System.out.printlnC'We got an exception while getting the metadata:" +
"check the connection."):
se.printStackTrace();
System.exit(l):
}
String columnName = null.
columnType = null:
try {
columnName = rsmd.getColumnName(1):
columnType - rsmd.getColumnTypeName(l):
} catch (SQLException se) {
System.out.printlnC'We got an exception while getting the column name:" +
"check the connection."):
se.printStackTrace():
System.exit(l):
}
System.out.printC'The name of the first column is: '"):
System.out.print(columnName);
System.out.printlrK.....):
System.out.printC'The data type of the first column is: "):
System.out.println(columnType):
Класс ResultSetMetaData содержит много других полезных методов. Описания приведены в документации JDK API.
DatabaseMetaData
Наконец, класс DatabaseMetaData предназначен для получения информации о базе данных, с которой вы работаете. В частности, он позволяет получить ответ па перечисленные ниже вопросы.
Какие каталоги присутствуют в базе данных? С каким типом базы я работаю? Под каким именем пользователя я работаю с базой данных?В листинге 12.8 объект DatabaseMetaData используется для получения у драйвера JDBC имени пользователя, с которым было создано подключение, и URL-адреса базы данных.
Листинг 12.8. Использование объекта DatabaseMetaData
DatabaseMetaData dbmd = null:
try {
dbmd = c.getMetaData():
} catch (SQLException se) {
System.out.printlnC'We got an exception while getting the metadata:" +
" check the connection."):
se.printStackTrace():
System.exit(l): }
String username = null:
try {
username = dbmd.getUserName():
} catch (SQLException se) {
System.out.printlnC'We got an exception while getting the username:" +
"check the connection."); se.printStackTraceO;
System.exit(l):
}
String url = null;
try {
url = dbmd.getURLO;
} catch (SQLException se) {
System.out.printlnC'We got an exception while getting the URL:" +
"check the connection.");
se.printStackTrace();
System.exit(l):
}
System.out.printlnC'You are connected to '" + url +
'" with user name '" + username + .....);
Как было сказано выше, лучшим источником информации о других методах DatabaseMetaData является документация JDK API.
Специфика использования JDBC с PostgreSQL
Данный раздел посвящен трем проблемам, часто возникающим при использовании JDBC с PostgreSQL. Первая проблема относительно проста. В объектах Resul tSet, возвращаемых в результате вызова executeQuery, указатель текущей записи всегда устанавливается в позицию перед первой записью, возвращенной запросом. Таким образом, перед выборкой данных из Resul tSet необходимо перейти к первой записи методом next. Пример приведен в листинге 12.9. При первом вызове getStri ng возникает исключение, поскольку текущей записи в наборе нет. После вызова next функция getStri ng успешно возвращает значение из первой записи набора.
Листинг 12.9. Выборка данных из первой записи средствами JDBC
ResultSet newSet = null;
try {
newSet = s.executeQuery("SELECT * FROM book"):
} catch (SQLException se) {
System.out.printlnC'We got an exception while executing our query:" +
"This probably means that our SQL is invalid.");
Специфика использования JDBC с PostgreSQL
se.printStackTrace() :
System.exit(1):
}
try {
String value = newSet.getString(l); // ПЛОХО: метод next() еще не вызывался
} catch (Exception e) {
System.out.phntlnC'We'll get an exception here, because we haven't" +
" stepped to the first row of the ResultSet yet.");
e.printStackTrace():
}
try {
newSet.next():
String value - newSet.getString(l);
} catch (SQLException se) {
System.out.printlnC'We'll only get an exception here if we've lost" +
"our connection, which isn't our fault.");
se.printStackTrace();
System.exit(l);
}
Следующая проблема тоже относится к объектам ResultSet, но она значительно проще первой. Вы не можете узнать количество записей, возвращенных в результате запроса, без вызова next с увеличением счетчика. Другими словами, в классе ResultSet не существует простого метода для получения количества записей в наборе. Отчасти это связано с тем, что JDBC может и не получать записи от PostgreSQL (и вообще не знать о существовании таких записей) до вызова next.
Последняя проблема менее очевидна. В многопоточных программах каждый программный поток должен работать с собственными объектами Statement и ResultSet. Дело в том, что в этих объектах хранятся данные состояния, и обращения к ним со стороны других программных потоков могут привести к повреждению этих данных.