Доступ к базам данных из Javaпрограмм и проблемы русификации
ВведениеРазумеется, что организовать доступ к базам данных из современного языка программирования в наше время не представляет никакой сложности. Более того, и сами языки программирования более всего оцениваются разработчиками по типу и возможностям заложенных в них средств доступа к базам данных, удобству и полноте интерфейсов. В этом смысле Java не представляет исключения. Уже в версии JDK1.1 появился пакет классов java.sql, обеспечивающий больщинство функций, известных к тому времени разработчикам ODBC-приложений. В этом пакете содержится ряд замечательных классов, например: java.sql.CallableStatement, который обеспечивает выполнение на Java хранимых процедур; java.sql.DatabaseMetaData, который исследует базу данных на предмет ее реляционной полноты и целостности с получением самых разнообразных данных о типах и содержимом таблиц, колонок, индексов, ключей и т.д.; наконец, - java.sql.ResultSetMetaData, с помощью которого можно выводить в удобном виде всю необходимую информацию из таблиц базы данных или печатать сами метаданные в виде названий таблиц и колонок.
Однако, коренное отличие Java от других традиционных языков программирования заключается в том, что одни и те же функции доступа к базам данных, с помощью универсальности и кроссплатформенности Java, можно организовать чрезвычайно гибко, используя все преимущества современных объектно-ориентированных технологий, WWW и Intranet/Internet. Рассмотрим по порядку все варианты использования Java-программ при взаимодействии с базами данных.
Java, инкапсулированная в СУБД
2.1 Java, инкапсулированная в СУБДJava может быть встроена в СУБД множеством различных способов и при этом всегда достигается решение сразу нескольких задач:
Следующий пример вставляет новую запись в таблицу:
INSERT INTO employees (id, name, Address) VALUES(1789, ыSerg Dunaev«, new Rus_Address(ы58 Gagarin Street«, ы153038«) )
Следующий запрос с использованием Java-объектов возвращает список сотрудников, живущих на указанной улице:
SELECT name FROM employees WHERE Rus_Address.street="Gagarin Street"
Java-программы и апплеты с интерфейсом JDBC-ODBC
1. Java-программы и апплеты с интерфейсом JDBC-ODBCJDBC (Java Database Connectivity) является не протоколом, а интерфейсом и основан на спецификациях SAG CLI (SQL Access Group Call Level Interface - интерфейс уровня вызова группы доступа SQL).
Сам по себе JDBC работать не может и использует основные абстракции и методы ODBC. Хотя в стандарте JDBC API и предусмотрена возможность работы не только через ODBC, а и через использование прямых линков к базам данных по двух- или трех-звенной схеме (см. Рисунок 1), эту схему используют гораздо реже, чем повсеместно используемый JDBC-ODBC-Bridge занимающий центральное место в общей схеме взаимодействия интерфейсов (см. Рисунок 2)

Java-сервлеты
3. Java-сервлетыЕсли апплеты расширяют функциональность Web-браузеров, то сервлеты расширяют функциональность Web-серверов и являются мощным средством программирования. В последнее время многие предпочитают обыкновенным апплетам, загружаемым локально или удаленно, именно сервлеты, которые не нужно никуда загружать и, которые всегда выполняются в контексте Web-сервера, обеспечивая, в отличие от обычных CGI-процессов или скриптов, куда более развитые возможности.
Как работает RMI
Как работает RMIВы определяете Java-интерфейс, чтобы описать каждый объект, который будет дистанционно разделяем, и перечисляeте общие методы, которые могут быть вызваны для объекта. Сервер будет использовать RMI-интерфейс и создаст объекты для вызова, специальным образом зарегистрированные и доступные для вызова по URL-основанной схеме, например:
rmi://localhost/LookupServer
Клиент, используя эту запись, будет пытаться отыскать объект с данным именем, и получить удаленную ссылку к нему. Затем вызванный метод будет обработан с помощью RMI компилятора и преобразован из пользовательского кода в последовательную форму объекта, который передается пользователю с помощью TCP/IP.
Разработка серверного кода
Разработка серверного кодаПосле того как вы определили интерфейс к удаленному объекту, нужно выполнить следующий шаг - разработать код сервера. Сервер осуществляет объектный интерфейс и создает образцы объекта, который будет дистанционно-разделяем.
Для нашего примера это будет выглядеть следующим образом:
// LookupServer.java
import java.io.*; import java.util.*; import java.rmi.*; import java.rmi.server.*;
public class LookupServer extends UnicastRemoteObject implements Lookup { private Vector save = new Vector();
public LookupServer(String db) throws RemoteException { try { FileReader fr = new FileReader(db); BufferedReader br = new BufferedReader(fr); String s = null; while ((s = br.readLine()) != null) save.addElement(s); fr.close(); } catch (Throwable e) { System.err.println(ыexception«); System.exit(1); } }
public String findInfo(String info) { if (info == null) return null;
info = info.toLowerCase(); int n = save.size(); for (int i = 0; i < n; i++) { String dbs = (String)save.elementAt(i); if (dbs.toLowerCase().indexOf(info) != -1) return dbs; }
return null; }
public static void main(String args[]) { try { RMISecurityManager security = new RMISecurityManager(); System.setSecurityManager(security); String db = args[0]; LookupServer server = new LookupServer(db); Naming.rebind(ыLookupServer«, server); System.err.println(ыLookupServer ready...«); } catch (Throwable e) { System.err.println(ыexception: ы + e); System.exit(1); } } }
Сервер читает в текстовой базе номера телефонов и имена и сохраняет их внутренне. Метод findInfo ищет затем нужное имя и телефон.
Пример базы данных:
Ivanov, Ivan 295-0083
Petrov, Peter 775-9958
Romanov, Alexander 555-7779
Заметим, что LookupServer является расширением стандартного класса java.rmi.server.UnicastRemoteObject и выполняет Lookup. Один из этих классов обеспечивает некоторые базисные реквизиты, необходимые для удаленных объектов, а другой определяет методы, которые будут вызваны дистанционно.
Разработка удаленного объектного кода
Разработка удаленного объектного кодаRMI поддерживает объекты Java, осуществляющие связь через их методы, независимо от того, где эти объекты размещены. Первый шаг для создания класса, к которому можно обратиться дистанционно, - это определение интерфейса, описывающего методы, которые являются дистанционно-разделяемыми.
// Lookup.java
import java.rmi.*;
public interface Lookup extends Remote { public String findInfo(String info) throws RemoteException; }
Java.rmi.Remote - пустой интерфейс, который указывает, что это удаленный объект, - так объекты класса, выполняющие Поиск(ыLookup«) отмечены удаленными ссылками. Все методы в удаленном интерфейсе должны быть объявлены через исключение типа Java.rmi.RemoteException, которое активизируется всякий раз, когда метод удаленного вызова дает сбои.
Непосредственный доступ к базе данных по 3-х-звенной схеме.
Рисунок 1. Непосредственный доступ к базе данных по 3-х-звенной схеме.
Схема взаимодействия интерфейсов.
Рисунок 2. Схема взаимодействия интерфейсов.Даже беглого взгляда на Рисунок 2 вполне достаточно, чтобы понять - общая схема взаимодействия интерфейсов в Java удивительным образом напоминает столь всем знакомую схему ODBC с ее гениальным изобретением драйвер-менеджера к различным СУБД и единого универсального пользовательского интерфейса. JDBC Driver Manager - это основной ствол JDBC-архитектуры. Его первичные функции очень просты - соединить Java-программу и соответствующий JDBC драйвер и затем выйти из игры. Естественно, что ODBC был взят в качестве основы JDBC из-за его популярности среди независимых поставщиков программного обеспечения и пользователей. Но тогда возникает законный вопрос - а зачем вообще нужен JDBC и не легче ли было организовать интерфейсный доступ к ODBC-драйверам непосредственно из Java? Ответом на этот вопрос может быть только однозначное нет. Путь через JDBC-ODBC-Bridge, как ни странно, может оказаться гораздо короче.
1. ODBC нельзя использовать непосредственно из Java, поскольку он основан на C-интерфейсе. Вызов из Java C-кода нарушает целостную концепцию Java, пробивает брешь в защите и делает программу трудно-переносимой.
2. Перенос ODBC C-API в Java-API нежелателен. К примеру, Java не имеет указателей, в то время как в ODBC они используются.
3. ODBC слишком сложен для понимания. В нем смешаны простые и сложные вещи, причем сложные опции иногда применяются для самых простых запросов.
4. Java-API необходим, чтобы добиться абсолютно чистых Java решений. Когда ODBC используется, то ODBC-драйвер и ODBC менеджер должны быть инсталлированы на каждой клиентской машине. В то же время, JDBC драйвер написан полностью на Java и может быть легко переносим на любые платформы от сетевых компьютеров до мэйнфреймов.
JDBC API - это естественный Java-интерфейс к базовым SQL абстракциям и, восприняв дух и основные абстракции концепции ODBC, он реализован, все-таки, как настоящий Java-интерфейс, согласующийся с остальными частями системы Java.
В отличие от интерфейса ODBC, JDBC организован намного проще. Главной его частью является драйвер, поставляемый фирмой JavaSoft для доступа из JDBC к источникам данных. Этот драйвер является самым верхним в иерархии классов JDBC и называется DriverManager. Согласно, установившимся правилам Internet, база данных и средства ее обслуживания идентифируются при помощи URL.
jdbc::
import java.net.URL; import java.sql.*; import java.io.*;
class SimpleSelect {
public static void main (String args[]) { String url = ыjdbc:odbc:dBase«; String query = ыSELECT * FROM my_table«;
try {
// Загрузка jdbc-odbc-bridge драйвера
Class.forName (ыsun.jdbc.odbc.JdbcOdbcDriver«);
DriverManager.setLogStream(System.out);
// Попытка соединения с драйвером. Каждый из // зарегистрированных драйверов будет загружаться, пока // не будет найден тот, который сможет обработать этот URL
Connection con = DriverManager.getConnection ( url, ы«, ы«);
// Если не можете соединиться, то произойдет exception // (исключительная ситуация). Однако, если вы попадете // в следующую строку программы, значит вы успешно соединились с URL
// Проверки и печать сообщения об успешном соединении //
checkForWarning (con.getWarnings ());
// Получить DatabaseMetaData объект и показать // информацию о соединении
DatabaseMetaData dma = con.getMetaData ();
//System.out.println(ы\nConnected to ы + dma.getURL()); //System.out.println(ыDriver ы + //dma.getDriverName()); //System.out.println(ыVersion ы + //dma.getDriverVersion()); //System.out.println(ы«);
// Создать Оператор-объект для посылки // SQL операторов в драйвер
Statement stmt = con.createStatement ();
// Образовать запрос, путем создания ResultSet объекта
ResultSet rs = stmt.executeQuery (query);
// Показать все колонки и ряды из набора результатов
dispResultSet (rs);
// Закрыть результирующий набор
rs.close();
// Закрыть оператор
stmt.close();
// Закрыть соединение
con.close(); } catch (SQLException ex) {
// Случилось SQLException. Перехватим и // покажем информацию об ошибке. Заметим, что это // может быть множество ошибок, связанных вместе //
//System.out.println (ы\n*** SQLException caught ***\n«);
while (ex != null) { //System.out.println (ыSQLState: ы + // ex.getSQLState ()); //System.out.println (ыMessage: ы + ex.getMessage ()); //System.out.println (ыVendor: ы + //ex.getErrorCode ()); ex = ex.getNextException (); //System.out.println (ы«); } } catch (java.lang.Exception ex) {
// Получив некоторые другие типы exception, распечатаем их.
ex.printStackTrace (); } }
//---------------------------------- // checkForWarning // Проверка и распечатка предупреждений. Возврат true если // предупреждение существует //----------------------------------
private static boolean checkForWarning (SQLWarning warn) throws SQLException { boolean rc = false;
// Если SQLWarning объект был получен, показать // предупреждающее сообщение.
if (warn != null) { System.out.println (ы\n *** Warning ***\n«); rc = true; while (warn != null) { //System.out.println (ыSQLState: ы + //warn.getSQLState ()); //System.out.println (ыMessage: ы + //warn.getMessage ()); //System.out.println (ыVendor: ы + //warn.getErrorCode ()); //System.out.println (ы«); warn = warn.getNextWarning (); } } return rc; } //---------------------------------- // dispResultSet // Показать таблицу полученных результатов //---------------------------------- private static void dispResultSet (ResultSet rs) throws SQLException, IOException { // Объявление необходимых переменных и // константы для желаемой таблицы перекодировки данных int i, length, j; String cp1 = new String(ыCp1251«); // Получить the ResultSetMetaData. Они будут использованы // для печати заголовков ResultSetMetaData rsmd = rs.getMetaData (); // Получить номер столбца в результирующем наборе int numCols = rsmd.getColumnCount (); // Показать заголовок столбца for (i=1; i<=numCols; i++) { if (i > 1) System.out.print(ы,«); //System.out.print(rsmd.getColumnLabel(i)); } System.out.println(ы«);
// Показать данные, загружая их до тех пор, пока не исчерпается // результирующий набор
boolean more = rs.next (); while (more) {
// Цикл по столбцам
for (i=1; i<=numCols; i++) {
// Следующая группа операторов реализует функции перекодировки // строк из таблицы базы данных в желаемый формат, потому что в // различных базах символы могут быть закодированы произвольным // образом. Если использовать стандартный метод - getString - на выходе // получается абракадабра. Строки нужно сначала перевести в Unicode, // затем конвертировать в строку Windows и убрать лидирующие нули
InputStream str1 = rs.getUnicodeStream(i); byte str2[]; byte str3[]; int sizeCol = rsmd.getColumnDisplaySize(i); str2 = new byte[sizeCol+sizeCol]; str3 = new byte[sizeCol+sizeCol]; length = str1.read(str2);
// Здесь нужно убрать нули из строки, которые предваряют каждый // перекодированный символ k=1; for (j=1; j
String str = new String(str3,cp1); System.out.print(str); } System.out.println(ы«);
// Загрузка следующего ряда в наборе
more = rs.next (); } }
}
В этой простой программе, приводимой во множестве руководств, мною произведено одно небольшое изменение, позволяющее использовать ее для работы с различными базами данных, содержащих таблицы с полями в кириллической кодировке. Дело в том, что хотя Java автоматически производит преобразования из Unicode и обратно в соответствии с установленными на вашей машине языковыми спецификациями (так называемые locale), эти преобразования не всегда действуют по отношению к кириллическим фонтам, особенно, когда кириллические строки прописаны не непосредственно в Java-программе, а передаются из внешних источников, например из баз данных через несколько промежуточных слоев. Та же проблема, как мы увидим далее, возникает и при использовании сервлетов, работающих в тесной взаимоувязке с Web-серверами.
Возможные процессы на Web-сервере.
Рисунок 3. Возможные процессы на Web-сервере.Несмотря на то, что сервлеты используют HTTP-протокол, им нет необходимости перезагружать процесс при каждом новом запросе и это также повышает их быстродействие. К сожалению, когда вы используете сервлеты, опять-таки, возникает проблема использования кириллических символов. Дело в том, что стандартный путь прохождения данных тут не действует, потому что Web-сервер при старте запускает так называемый Java-handler, которому передает на выполнение все Java-сервлеты и обменивается вводом-выводом именно с ним через специальную библиотеку классов. Поэтому, если вы хотите получать в браузере от вашего Web-сервера, который исполняет сервлеты, кириллические строки, вам надо использовать вместо стандартных примитивов ввода/вывода некоторые специальные методы для работы со строками, например:
// Определение необходимого кодификатора // в зависимости от используемого типа кодировки String dos = new String(ыCp866«); String win = new String(ыCp1251«); String iso = new String(ы8859_5«); String im = new String(ыКириллический текст«); ....................................................... out.println(ы«); //out.write(im.getBytes(dos)); out.write(im1.getBytes(win)); //out.write(im2.getBytes(iso)); out.println(ы