su13@pochta.ru

| Первый | Второй | Третий | Четвёртый | Пятый | Шестой | Седьмой |


Глава №14.

Java u JDBC

В главе 13 «Си C++», мы познакомили вас с С API для MySQL и mSQL. К несчастью, каждый API позволяет писать программы только для той базы данных, которую он поддерживает. Если вы собираетесь переносить приложение между MySQL и mSQL или, того хуже, хотите, чтобы оно работало на Oracle, Sybase или с любой другой СУБД, вам необходимо переписать свой код так, чтобы он использовал фирменный API этого ядра. Однако Java-программисты по большей части избавлены от проблем переносимости на другую базу данных. У них есть единый API, Java DataBase Connectivity API (JDBC), обеспечивающий их унифицированным интерфейсом ко всем SQL-базам данных.

Поскольку JDBC является единым интерфейсом ко всем базам данных, достаточно изучить его, чтобы писать приложения, которые будут работать как с MySQL, так и с mSQL. На самом деле, если должным образом использовать JDBC, то написанные вами на Java приложения смогут работать с любой СУБД. Если у вас есть доступ к другим базам данных, кроме MySQL и mSQL, можете проверить верность этого утверждения, запустив примеры данной главы с другой базой данных.

Для чтения этой главы предполагается знание основ языка программирования Java и лежащих в его основе концепций. Если такой подготовки у вас нет, настоятельно рекомендуем посмотреть «Exploring Java» (O'Reilly & Associates, Inc.). Узнать более подробно о том, как создавать многоуровневые приложения баз данных, о чем мы рассказывали в главе 8 «Архитектуры приложений баз данных», можно из книги «Database Programming with JDBC and Java» (O'Reilly & Associates, Inc.).

Что такое JDBC?

Как и все Java API, JDBC является набором классов и интерфейсов, в совокупности поддерживающих определенный набор функций. В случае JDBC эти функции обеспечивают доступ к базе данных. Классы и интерфейсы, составляющие JDBC API, являются, таким образом, абстракциями понятий, общих при доступе к базам данных любого типа. Например, Connection является интерфейсом Java, представляющим соединение с базой данных. Аналогично ResultSet представляет результирующий набор данных, возвращаемый командой SQL SELECT. Классы, образующие JDBC API, находятся в пакете Java, sql, который был введен Sun в JDK 1.1.

Естественно, что конкретные детали доступа к базе данных зависят от ее изготовителя. JDBC фактически не имеет дела с этими деталями. Большая часть классов в пакете Java.sql является интерфейсами без реализации. Реализация этих интерфейсов осуществляется производителем базы данных в виде драйвера JDBC. В качестве программиста баз данных вам нужно знать очень немногое относительно драйвера, который вы используете, — все остальное делается через интерфейсы JDBC. Специфическая информация о базе данных, которая необходима для использования JDBC, включает в себя:

В новую спецификацию JDBC 2.0 включено необязательное для реализации производителями баз данных стандартное расширение API. Если поставщик вашей базы данных JDBC реализовал это стандартное расширение, вам даже нет необходимости знать JDBC URL или реализацию класса Driver. Это расширение предусматривает наличие класса DataSource, который можно найти по имени в каталоге с поддержкой JNDI.

Оба эти элемента можно получить во время выполнения - из командной строки или файла свойств. Сам код программы не ссылается на эти два зависящие от реализации элемента. Мы разъясним, что делают JDBC URL и класс Driver в тех параграфах, где будем рассказывать о соединении с базами данных. На рисунке 14-1 представлена схема интерфейсов JDBC.

JNDI - Java Naming and Directory Interface (интерфейс имен и каталогов Java) API. Он позволяет запоминать объекты Java в службе имен и каталогов, такой как сервер Lightweight Directory Access Protocol (облегченный протокол доступа к каталогам - LDAP), и находить их по имени.

Рис. 14-1. Классы и интерфейсы, входящие в JDBC API

Соединение с базой данных

Прежде всего нужно соединиться с базой данных. Один из немногих реализованных в пакете Java. sql. package классов - это класс DriverManager. Он поддерживает список реализаций JDBC и обеспечивает создание соединений с базами данных на основе сообщаемых ему JDBC URL. URL для JDBC имеет вид jdbc:protocol:subprotocol. Он сообщает DriverManager, с какой СУБД нужно соединиться, и передает ему данные, необходимые для осуществления соединения.

Смысл слова «driver» в JDBC зависит от контекста. При написании в нижнем регистре JDBC driver является собранием классов, в совокупности реализующих все интерфейсы JDBC и обеспечивающих приложение доступом хотя бы к одной базе данных. При написании Driver с заглавной буквы подразумевается класс, реализуемый в Java. sql. Driver. И наконец, JDBC предоставляет интерфейс DriverManager, с помощью которого можно вести учет всех различных реализаций Driver.

Часть URL, обозначающая протокол, ссылается на конкретный драйвер JDBC. В случае MySQL и mSQL протоколами являются ту sql и msql соответственно. Субпротокол сообщает данные соединения, специфические для реализации. Для соединения с MySQL и mSQL требуются имя узла и имя базы данных. Дополнительно может потребоваться номер порта, если ядро базы данных запущено не как root. Поэтому полный URL для mSQL выглядит как, например, jdbc:msql://athens.imagi-nary.com: 1114/test. Он сообщает DriverManager о необходимости найти драйвер JDBC для mSQL и соединиться с базой данных test на athens.imaginary.com через порт 1114. Это делается путем единственного обращения к методу getConnection() интерфейса DriverManager. В примере 14-1 показано, как осуществить соединение с базой данных mSQL.

Пример 14-1. Отрывок кода из примеров, предоставляемых с драйвером JDBC для mSQL, показывающий, как осуществить соединение

import java.sql.*;

public class Connect { public static void main(String argv[]) {

Connection con = null;

try {

// Вот JDBC URL для этой базы данных

String url = "jdbc:msql://athens.imaginary.com:1114/db_test";

// 0 том, что делают классы Statement и ResultSet, ниже Statement stmt; ResultSet rs;

// передать это как свойство, т.е.

// -Djdbc.drivers=com.imaginary.sql.msql.MsqlDriver

// или загрузить, как сделано в этом примере

Class.fоrName("com.imaginary, sql. msql. MsqlDriver");

// здесь осуществляется соединение

con = DriverManager.getConnection(url, "borg", "");

}

catch( SQLException e ) {

e.printStackTrace(); }

finally {

if( con != null ) {

try { con.close();

}

catch( Exception e ) { }

}

}

}

}

В этом примере соединение с базой данных осуществляется в строке con=DriverManager.getConnection(url, "borg", ""). В данном случае JDBC URL и имя класса, реализующего Driver, фактически введены в код приложения. В демонстрационной программе это допустимо, но всякое серьезное приложение должно загружать эти данные из файла свойств, получать через аргументы командной строки или из свойств системы. Реализация Driver будет автоматически загружена, если передать ее как системное свойство jdbc.drivers - иными словами, не нужно вызывать Class. ForName(). newlnstance(driver_name), если вы передаете имя драйвера как системное свойство jdbc.drivers. Второй и третий аргументы getConnection() передают ID пользователя и пароль, необходимые для установления соединения. Поскольку mSQL не использует пароли для авторизации пользователей, в примере используется пустая строка. Для MySQL же необходимо сообщить пароль.

Поддержка переносимости с помощью файлов свойств

Хотя наше внимание сосредоточено на двух конкретных базах данных, хорошей практикой программирования на Java является обеспечение полной переносимости приложений. Под переносимостью обычно подразумевается, что вы не пишете код, предназначенный для выполнения только на какой-то одной платформе. Однако для Java термин «переносимость» имеет более сильный смысл. Он означает независимость от аппаратных ресурсов и независимость от базы данных.

Мы сказали о том, что JDBC URL и имя Driver зависят от реализации, но не сказали, как избежать их включения в код. Поскольку и то, и другое представляет собой простые строки, их можно передать в качестве параметров командной строки или как параметры апплетов. Это работающее, но едва ли элегантное решение, поскольку оно требует, чтобы пользователь помнил длинные командные строки. Аналогичное решение - выдавать пользователю приглашение для ввода этих данных, которое опять-таки требует, чтобы пользователь вспоминал JDBC URL и имя класса Java при каждом запуске приложения.

Более изящное решение получается при использовании файла свойств. Файлы свойств поддерживаются классом Java. util. Resource-Bundle и его подклассами, позволяя приложению извлекать данные, относящиеся ко времени исполнения, из текстового файла. Для приложения, использующего JDBC, можно вставить в файл свойств URL и имя Driver, возложив на администратора приложения обязанность указать детали соединения. Пример 14-2 показывает файл свойств,

предоставляющий данные о соединении.

Пример 14-2. Файл SelectResource.properties с подробностями соединения

Driver=com.imaginary.sql.msql.MsqlDriver

URL=jdbc:msql://athens.imaginary.com:1114/db_test

В примере 14-3 показан переносимый класс Connection.

Пример 14-3. Специфические данные

import java.sql.*; import java.util.*;

public class Connect {

public static void main(String argv[]) {

Connection con = null;

ResourceBundle bundle = ResourceBundle.getBundle("SelectResource");

try {

String url = bundle.getString("URL");

Statement stmt; ResultSet rs;

Class.forName(bundle.getString("Driver")); // здесь осуществляется соединение

con = DriverManager.getConnection(url, "borg", ""); }

catch( SQLException e ) { e. printStackTrace();

}

finally

{

if( con != null )

{

try { con.close(); }

catch( Exception e ) { }

}

}

}

}

В этом примере установления соединения мы избавились от кода, специфичного для mSQL. Однако для разработчиков переносимых JDBC-приложений остается одна важная проблема, особенно касающаяся тех, кто работает с mSQL. JDBC требует, чтобы все драйверы поддерживали начальный уровень (entry level) SQL2. Это стандарт ANSI минимальной поддержки SQL. Если при вызовах JDBC вы поддерживаете начальный уровень SQL2, то ваше приложение будет стопроцентно переносимо на другие базы данных. MySQL поддерживает минимальный уровень SQL2, a mSQL - увы, нет. Приложения, написанные для mSQL, скорее всего, без проблем будут переноситься на другие базы данных, но приложения, написанные с использованием начального уровня SQL92, в полном объеме нельзя будет безболезненно перенести обратно на mSQL.

Простой доступ к базе данных

В примере Connect делалось не много. В нем было просто показано, как соединиться с базой данных. В соединении с базой данных нет пользы, пока вы не начинаете действительно обмениваться с ней данными. Простейшие виды доступа к базе данных - команды SELECT, INSERT, UPDATE и DELETE. В JDBC API вы используете экземпляр Connection для создания экземпляров класса Statement. Класс Statement представляет SQL-команду любого типа. В примере 14-4 показано, как вставить строку в базу данных, используя Statement.

Пример 14-4. Вставка строки в mSQL с помощью объекта JDBC Statement

import Java, sql.*;

import Java, util.*;

public class Insert {

// Делаем вставку в таблицу, имеющую две колонки: test_id (int)

// и test_val (char(55))

// args[0] - это test_id, a args[1] - test_val

public static void main(String argv[]) {

Connection con = null;

ResourceBundle bundle = ResourceBundle.getBundle("SelectResource");

try {

String url = bundle.getString("URL");

Statement stmt;

Class.forName(bundle.getString("Driver"));

// здесь осуществляется соединение

con = DriverManager.getConnection(url, "borg", "");

stmt = con.createStatement();

stmt.executeUpdate("INSERT INTO test (test_id, test_val) " +

"VALUES(" + args[0] + ", '," + args[1] + ")");

}

catch( SQLException e )

{

e. printStackTrace();

}

finally

{

if( con != null )

{

try { con.close();

}

catch( Exception e ) { }

}

}

}

}

В реальном приложении мы бы, конечно, проверили, что пользователь ввел значение типа INT для test_id, что оно уникально, и что длина введенного значения test_val не превышает 55 символов. Тем не менее пример показывает, как просто осуществлять вставку данных. Метод createStatement() делает то, о чем говорит его название: создает пустую SQL-команду, связанную с рассматриваемым соединением - объектом Connection. Затем метод executeUpdate() передает заданную строку SQL базе данных для выполнения. Как подсказывает название, executeUp-date() ожидает команды SQL, которая некоторым образом модифицирует базу данных. Вы можете использовать ее, чтобы вставлять новые строки, как показано выше, либо удалять строки, обновлять строки, создавать новые таблицы или производить любые другие изменения в базе данных.

Запросы выполнять немного сложнее, чем обновления, поскольку они возвращают информацию из базы данных в виде объекта ResultSet. ResultSet является интерфейсом, представляющим 0 или более строк, являющихся результатом запроса, обращенного к базе данных. В классе JDBC Statement имеется метод executeQuery(), работающий подобно executeUpdate(), за исключением того, что он возвращает из базы данных ResultSet. Метод executeQuery() возвращает ровно один ResultSet, тем не менее имейте в виду, что JDBC поддерживает извлечение множественных результирующих наборов для тех баз данных, которые это позволяют. Ни MySQL, ни mSQL не поддерживают множественные результирующие наборы. Однако важно помнить о такой возможности, когда вы изучаете код для работы с иным ядром базы данных, написанный кем-то другим. В примере 14-5 показан простой запрос. На рис. 14-2 изображена модель данных з таблице test.

Пример 14-5. Простой запрос

import Java, sql.*;

import Java, util.*;

public class Select {

public static void main(String argv[]) {

Connection con = null;

ResourceBundle bundle =ResourceBundle.getBundle("SelectResource");

try {

String url = bundle.getString("URL"); Statement stmt;

ResultSet rs;

Class.forName(bundle.getString("Driver")); // здесь осуществляется соединение

con = DriverManager,getConnection(url, "borg", "");

stmt = con.createStatement();

rs = stmt .executeQuery("SFI FCT* from test ORDER BY test_id");

System, out.print In("Полученные результаты:");

while(rs. next()) {

int a= rs.getInt("test_icT);

String str = rs.getString("test_val");

System.out.print(" ключ= " + a);

System.out.print(" строка= " + str);

System.out.print("\n");

}

stmt.close();

}

catch( SQLException e )

{

e. printStackTrace();

}

finally {

if( con != null ) {

try { con.close(); }

catch( Exception e ) { }

}

}

}

}

Приложение Select выполняет запрос и затем проходит по всем строкам ResultSet с помощью метода next(). До первого обращения к next() ResultSet не указывает ни на какую строку. Каждый вызов next () настраивает ResultSet на следующую строку. JDBC 2:0 вводит понятие результирующего набора с перемещением (scrollable). Если ваш экземпляр ResultSet позволяет перемещение, можно также обращаться к методу previous() для перемещения по результирующему набору в обратном направлении. Обработка строк заканчивается, когда next () возвращает false.

Рис. 14-2. Таблица test из учебной базы данных

Работа со строкой означает получение значений для каждой колонки. Каково бы ни было значение в базе данных, можно использовать методы ResultSet для получения значения колонки с любым типом данных Java, который вас устраивает. В приложении Select вызов метода gе tInt() возвращал колонку test_id как int, а вызов getString() возвращал значение колонки test_val как String. Эти методы получения значения колонки принимают либо номер колонки, начиная с 1, либо ее имя. Следует, однако, всеми силами избегать извлечения значений с помощью имен колонок, поскольку такой способ значительно медленнее, чем получение их с помощью номеров колонок.

Обработка ошибок и освобождение ресурсов

Все методы JDBC могут возбуждать SQLException или один из подклассов этого класса, если что-то происходит при обращении к базе данных. Ваш код должен уметь перехватывать исключительную ситуацию, обрабатывать ее и освобождать все размещенные в памяти ресурсы базы данных. Все перечисленные до сих пор классы JDBC имеют метод close(). Однако на практике вы должны обеспечить закрытие только тех объектов, которые вызваны процессами, продолжающими оставаться открытыми. В приведенных до сих пор примерах практически требовалось закрыть только соединение с базой данных. При закрытии соединения автоматически закрываются все связанные с ним команды и результирующие наборы. Однако если вы намерены сохранить соединение открытым в течение некоторого времени, будет правильным поспешить закрыть все объекты statement, которые вы создали с использованием этого соединения, когда они вам больше не нужны. В примерах JDBC, которые вы видели, это освобождение ресурсов производится в предложении finally. Это делается для того, чтобы обеспечить закрытие соединения независимо от того, что произойдет.

Динамический доступ к базе данных

До сих пор мы имели дело с приложениями, в которых во время компиляции точно известно, что нужно будет делать. Если бы это был единственный тип поддержки, обеспечиваемый JDBC, никто не смог бы написать для Mysql и msql интерактивные инструменты командной строки, способные во время исполнения принимать команды SQL и выполнять их. Класс JDBC Statement поддерживает метод execute() для выполнения SQL-команд, которые могут быть запросами или обновлениями. Кроме того, экземпляры ResultSet обеспечивают предоставление о себе информации времени исполнения через интерфейс с именем ResultSetMetaData, доступ к которому осуществляется через вызов метода getMetaData() для ResultSet.

Метаданные

Термин метаданные звучит официозно, но на самом деле это не более чем дополнительные данные о некотором объекте, которые, если бы действительно хранились в объекте, просто зря тратили бы ресурсы. Например, для простых приложений не нужны имена колонок, связанных с результирующим набором ResultSet: программисту они, скорее всего, известны во время написания программы. Поэтому помеще

ние этих дополнительных данных в класс ResultSet не рассматривается разработчиками JDBC как нечто существенное для функциональности ResultSet. Однако в некоторых случаях программирования баз данных i такие вещи, как имена колонок, очень важны, особенно при осуществлении динамического доступа к базам данных. Доступ к этим дополнительным данным - метаданным - разработчики JDBC обеспечили через интерфейс ResultSetMetaData. Этот класс позволяет узнать:

Другим примером класса, поставляемым вместе с драйвером mSQL-JDBC, является приложение Exec. Оно принимает любую команду SQL, задаваемую в командной строке, и выполняет ее. В примере 14-6 приводится соответствующий исходный код.

Пример 14-6. Исходный код приложения Exec для выполнения динамического SQL

import java.sql.*;

public class Exec {

public static void main(String args[]) {

Connection con = null; String sql = "";

for(int i=0; i<args.length; i++) {

sql = sql + args[i];

if( i < args.length - 1 ) {

sql = sql + " ";

}

}

System, out. рrintln("Выполнение: " + sql);

try {

Class.forName("com.imaginary. sql.msql.MsqlDriver").newlnstance();

String url = "jdbc:msql://athens.imaginary.com:1114/db_test";

con = DriverManager.getConnection(url, "borg", "");

Statement s = con.createStatement();

if( s.execute(sql) ) {

ResultSet r = s.getResultSet();

ResultSetMetaData meta = r.getMetaData();

int cols = meta.getColumnCount();

int rownum = 0;

while( r.next() ) { rownum++;

System, out. println("Crpокa: " + rownum);

for(int i=0; i<cols; i++) {

System.out.print(meta.getColumnLabel(i+1) + ": ' + r.getObject(i+1) + ", ");

}

System.out.println("");

}

}

else

{

System.out.println(s.getUpdateCount() + " строк обработано.");

}

s.close();

con.close();

}

catch( Exception,e ) { e. printStackTrace();

}

finally

{

if( con != null )

{

try { con.close();

}

catch( SQLException e ) { }

}

}

}

}

Для каждого результирующего набора обеспечивается получение экземпляра ResultSetMetaData с помощью метода getMetaData(). При динамическом доступе к базе данных необходимо узнать, сколько колонок в результирующем наборе, чтобы с уверенностью извлечь все колонки и их имена для вывода пользователю. Метаданные нашего результирующего набора обеспечивают получение всех этих данных с помощью методов getColumnCount() и getColumnLabel().

Обработка динамического SQL

В примере 14-6 введено понятие динамического вызова SQL. Поскольку мы не знаем, будет это запрос на получение данных или обновление, нужно передать вызов SQL через метод execute (). Этот метод возвращает true, если команда возвратила результирующий набор, и false в противном случае. В нашем примере, если возвращается true, то приложение получает возвращаемый ResultSet через вызов метода getResultSet(). Затем приложение может перейти к обычной обработке результирующего набора. Если, напротив, команда произвела какую-либо модификацию базы данных, можно вызвать метод getUpdateCu-unt() для подсчета числа строк, модифицированных командой.

Серверное приложение гостевой книги

Вы, вероятно, немало слышали об апплетах Java. Однако в главе 8 мы говорили о том, что доступ к базам данных на стороне клиента является плохой идеей. В примеры к этой книге мы включили реальное приложение, использующее сведения о JDBC, изложенные в этой главе, для создания класса Java на стороне сервера. Такие приложения называются сервлеты (servlet). Хотя серверные приложения сами по себе не являются частью трехзвенной архитектуры, обсуждавшейся нами в главе 8, данный пример хорошо иллюстрирует возможности использования JDBC. Это серверное приложение является Web-страницей, позволяющей посетителям вашего сайта оставить свое мнение о нем. Оставленные комментарии могут просматриваться другими посетителями. Все, что вам нужно знать о серверных приложениях для понимания этого примера, это то, что метод doPost() обрабатывает события HTTP POST, a de-Get () обрабатывает события HTTP GET.

В этом приложении две части: часть get и часть post. В обеих частях производится вызов метода printComments() для показа комментариев, оставленных в гостевой книге. В этом методе мы находим нечто, не встречавшееся нам в простых предыдущих примерах: вызов метода wasNull() после каждого извлечения значения колонки. Как и можно предположить из названия, wasNull() возвращает t rue, если последнее извлеченное значение было NULL в смысле SQL. В вызовах, возвращающих объект Java, значение обычно будет NULL, если при чтении из базы данных был получен NULL. Использование wasNull() в таких случаях может показаться излишним. Однако для простых типов данных выборка может возвращать допустимое значение. Метод wasNull() позволяет узнать, не было ли в базе данных значения NULL. Например, NULL в колонке целого типа возвращает 0 при вызове getlnt(). Чтобы узнать, что было в колонке - 0 или NULL, нужно вызвать wasNull().

Оглавление

Оглавление

Оглавление

GNU OCXE GNU LINUX
Сайт управляется системой uCoz