При этом все детали работы с БД должны быть сосредоточены внутри нее.

Пример создания библиотечной иерархии классов иллюстрируется фрагментами исходного текста, поясняющими наиболее важные детали реализации. В ней нет детального описания JDBC, так как автором не ставилась цель написать что-то вроде учебника или справочника, которых и без того хватает [2], а привести пример, который может быть полезен при попытке создания с помощью JDBC чего-либо сложнее фрагментов программ, печатаемых в книгах по Java DataBase Connectivity. По многим причинам в ней нет и детального описания всех методов классов библиотеки. Рамки статьи не позволят вместить полное описание классов.

Современные информационные системы получают данные из баз с помощью специальных программных интерфейсов. Когда к подобному интерфейсу предъявляются требования быть универсальным поставщиком информации из БД различных форматов, то, как правило, используют интерфейсы JDBC (Java DataBase Connectivity) компании Sun Microsystems или ODBC корпорации Microsoft.

Во многих случаях для получения одинаковых результатов JDBC предпочтительнее, поскольку он намного проще в освоении и эксплуатации: для одной и той же задачи можно обойтись в несколько раз меньшим числом строк Java, чем при использовании ODBC и языка программирования Си. Справедливости ради следует отметить, что программистами наработано много «методов борьбы» с ODBC, заключающихся как в применении библиотек функций без объектно-ориентированного подхода [1], так и в задействовании библиотек классов, реализованных на объектно-ориентированных языках типа Cи++. Это не только упрощает разработку приложений, но и делает получаемые программы надежнее — чем проще, тем лучше.

Что же до JDBC, то это уже готовая библиотека, инкапсулирующая и скрывающая в себе множество сложных деталей обращения к базам данных. Однако ничто не мешает расширить JDBC внешними библиотеками классов, что лишь усилит преимущества этого программного интерфейса.

Отлов ошибок

Пожалуй, самая рутинная операция, с которой сталкивается программист при использовании как ODBC, так и JDBC, — обработка ошибок:

java.sql.Connection con;
...
try
	{
	con.commit();
	}
catch (SQLWarning w)
	{
	...
	}	
catch (SQLException e)
	{
	...
	}

Из этого примера видно, что для корректной обработки ошибок необходимо перехватывать два исключения: SQLException и SQLWarning. К тому же часто нужно проанализировать ошибки, поступающие из разных источников, например несколько операций INSERT, завершающихся операцией COMMIT. Требования к классу DBErrorData, упорядочивающему обработку сообщений об ошибках, вывести достаточно легко:

  • класс должен содержать в себе значения типов SQLException и SQLWarning;
  • необходима возможность хранения множественных ошибок, т. е. последовательного накопления информации об ошибках и предупреждениях;
  • создаваемый класс должен уметь выдавать информацию обо всех накопленных ошибках и предупреждениях.

В соответствии с требованиями, показанными выше, прототип каркаса класса DBErrorData должен выглядеть так:

class DBErrorData {
private SQLException Excpt=null;
private SQLWarning Warn=null;
....
}
Метод resetErrors() присваивает этим полям значение null*.

Разумеется, в классе должно быть несколько полезных методов. Так, errorsArePresent() возвращает false, если оба поля Excpt и Warn равны первоначально инициализированному значению null, в противном случае возвращается true. Методы getException() и getWarning() возвращают соответственно значения полей Excpt и Warn. Методы getExceptionString() и getWarningString() возвращают текстовую информацию об ошибках и предупреждениях:

public String getExceptionString ( ) {
	String S;
	if(Excpt == null) return(null);
	S = Excpt.toString();
	Excpt = Excpt.getNextException();
	return S;
}

Однако раздельная обработка текстовых сообщений об ошибках и предупреждениях может оказаться неудобной, поэтому в классе присутствует метод getErrorString():

public String getErrorString( ) {
	if (false == errorsArePresent()) 
return(null);
	if(Excpt != null) return 
getExceptionString();
	if(Warn ! = null) return 
getWarningString();
	return(null);
}

Во всех вышеперечисленных методах нет самого главного — добавления исключений и предупреждений. Ниже показан метод addException(), использующий метод setNextException() класса SQLException для построения цепочки исключений:

public void addException 
( SQLException e ) {
	if(e == null) return;
	if(Excpt==null) Excpt = e;
	else Excpt.setNextException(e);
	return;
}

Аналогично строится и метод addWarning(), внутри которого используется setNextWarning(). Метод addErrorInfo() последовательно добавляет ошибки и предупреждения из иного экземпляра класса DBErrorData:

public void addErrorInfo( DBErrorData e ) {
this.addException(e.getException());
this.addWarning(e.getWarning());
}

Конечно, в классе можно предусмотреть дополнительные поля и методы для маскирования ошибок и предупреждений, выдачи сообщений о них средствами AWT или Swing и многих других целей. Так что приведенный набор средств является лишь необходимым минимумом.

Использование класса DBErrorData может быть проиллюстрировано так:

class Sample {
...
DBErrorData DBError;
java.sql.Connection con;
...
try
	{
	con.commit();
	}
catch (SQLWarning w)
	{
	DBError.addWarning(w);
	}	
catch (SQLException e)
	{
	DBError.addException(e);
	}
...		
}

Во всех классах библиотеки, обращающихся к функциям JDBC, обработка SQLException и SQLWarning ведется аналогично приведенному примеру. Начнем их рассмотрение с класса, включающего в себя экземпляр java.sql.Connection, так как работа JDBC в приложении начинается с установления соединения с базой данных. Из-за того что все запросы SQL (java.sql.Statement, PreparedStatement, CallableStatement) создаются не без участия класса Connection, представляется логичным поместить в класс DBConnection код, реализующий выполнение всех запросов. Помимо установления соединения с БД создаваемый класс выполняет не только стандартные операции по управлению базой для всех приложений (типа commit, rollback), но и запросы, необходимые для каждого конкретного приложения. С такими условиями класс довольно трудно унифицировать. Однако это возможно, если разделить его на два подкласса: DBConnection, отвечающий за установление соединения с БД и стандартные операции, и DBExchange, наследующий все это и включающий в себя запросы. Данная схема обеспечивает как наследуемость, так и гибкость в реализации приложений — для изменения запросов нужно только изменить методы класса DBExchange.

Начнем рассмотрение этих классов с родительского класса...

DBConnection

Поля данного класса приведены ниже:

class DBConnection {
//поля описаны как protected для доступа 
к ним класса-потомка
protected java.sql.connection Con =  null;
protected DBErrorData DBError = null;
}

Для IBM DB2 V2.1 конструктор класса выглядит так:

public DBConnection ( )
{
try
	{
	Class.forName(«COM.ibm.db2.jdbc.
app.DB2Driver»);
	//либо иная библиотека для другой БД
	}
catch (Exception e)
	{
	e.printStackTrace();	
	}
DBError = new DBErrorData();
}

Какие методы должны присутствовать в этом классе? Переписывать все методы java.sql.Connection на новый лад не нужно — достаточно обойтись теми, которые часто используются. Возможно введение в класс и общих для многих приложений операций по работе с БД, таких, как переформатирование и перекодировка данных и др. Для ясности методы в DBConnection лучше всего называть аналогично методам класса Connection либо названиями SQL-операций, например:

protected void setAutoСommit ( boolean val )
{
try	{	
	con.setAutoCommit(val);
	}
catch (SQLWarning w)
	{
	DBError.addWarning(w);	
	}	
catch(SQLException e )
	{
	DBError.addException(e);	
	}
}

Читатели могут спросить: «Чем это лучше простого вызова метода java.sql.Connection?» Главное достоинство приведенного текста — автоматизация сбора сообщений об ошибках и все ее положительные следствия. С уходом однажды написанного блока try/catch на уровень библиотеки приложение становится прозрачнее и легче в написании. Вследствие этого повышается и его надежность. Подобным образом реализовано большинство наследуемых классом DBExchange методов. Например, текст, устанавливающий соединение с базой, выглядит так:

protected void connect (String Url, 
String Usеrid, String Password )
{
try
	{
	Con = java.sql.DriverManager
.getConnection(Url,Userid,
Password);
	}
catch (SQLWarning w)
	{
	DBError.setWarning(w);	
	}	
catch(SQLException e )
	{
	DBError.setException(e);	
	}
return;
}
Дополнительно в классе описаны следующие константы:
public final static int DB_GET_FIRST = 1;
public final static int DB_GET_NEXT = 2;

Их применение будет приведено на примерах ниже. Закончив на этом рассмотрение класса DBConnection, начнем рассмотрение его потомка...

DBExchange

В этом классе должны присутствовать только поля и методы, необходимые для конкретного приложения. Конструктор класса DBExchange просто вызывает метод super(), инициализируя драйвер. Конечно, метод connect, устанавливающий связь с БД, может быть реализован так же легко — достаточно вызвать super.connect(...), но в нашей библиотеке данный метод сложнее. Дело в том, что сразу после установления соединения с базой становится возможной работа с SQL-операторами. Само собой, надо проинициализировать данные перед их использованием. Для данной цели в классе предусмотрен метод prepareStatements. Одновременная подготовка всех SQL-операторов очень удобна при дальнейшем сопровождении ПО — программисту достаточно отредактировать лишь метод prepareStatements, чтобы изменить запросы. Исходя из вышеизложенного, метод connect выглядит следующим образом:

public void connect(String Url, 
String Usеrid, String Password)
{
super.connect(Url,Userid,Password);
if (false == this.prepareStatements())
	{
	for(;;)
		{
		System.err.println(DBError
.getErrorString());
		If(false == DBError
.errorsArePresent()) break;
		}
	System.exit(1);
	}
}

Дело за малым — написать реализацию метода...

prepareStatements

В JDBC за выполнение SQL-операторов отвечают три класса: Statement, PreparedStatement, CallableStatement. Они имеют много общего: во всех классах этой троицы метод executeQuery возвращает ResultSet с данными, выдаваемыми по запросу, метод executeUpdate используется для операций вставки, изменения и удаления данных. Наконец, метод execute используется для сложных запросов, последовательно возвращающих несколько ResultSet. Работа с результатами запросов также одинакова для всех классов и сводится к извлечению данных из ResultSet. Напрашивается идея объединения запроса с результатами его работы в одном объекте. При этом реализуется принцип максимального скрытия работы с БД на «микроуровне» одного SQL-оператора. К тому же при выполнении запросов можно будет воспользоваться отлаженным механизмом сбора сообщений об ошибках. Однако при воплощении в жизнь этой задумки не следует забывать о различиях между классами Statement, PreparedStatement и CallableStatement. Для грамотной реализации этих различий воспользуемся проверенным методом — наследованием.

DBStmt

Класс DBStmt в библиотеке имеет трех потомков: DBStatement, DBPreparedStatement, DBCallableStatement.

Вот поля этого класса:

class DBStmt
{
protected DBErrorData DBError=null;
protected java.sql.ResultSet Res=null ;
protected java.lang.String SQLString=null;
}

Обратите внимание, что никаких классов JDBC, отвечающих за выполнение запросов, в этом описании нет. Они появятся только в классах-потомках. Рассмотрим методы данного класса в соответствии с используемыми ими полями. Методы setSQLString(String S) и getSQLString() соответственно устанавливают и возвращают значение SQLString. Методы getErrors() и clearErrors() возвращают и сбрасывают информацию об ошибках. Методы, работающие с ResultSet, названы аналогично методам самого ResultSet. Отметим, что вовсе не обязательно сразу реализовывать все методы, которые есть в ResultSet. Например, getDouble() можно смело опустить, если не планируется применение типа double. Однако при необходимости работы с этим типом придется дописывать нужный метод, поэтому не следует увлекаться «нереализацией» методов. Так, практически невозможно обойтись без getInt(), который и приведен в качестве примера:

public final int getInt 
( int ParamNumber ) {
int i = 0;
try
	{
 i = Res.getInt(ParamNumber);
	}
catch (SQLWarning w)
	{
	DBError.setWarning(w);
	}	
catch(SQLException e )
	{
	DBError.setException(e);
	}
return(i);	
}

В классе предусмотрены два конструктора, один из которых с параметром String сразу присваивает значение SQLString:

public DBStmt (String S) {
Res = null;
	DBError = new DBErrorData();
setSQLString(S);
// в конструкторе без параметров 
setSQLString(null);
 }

Классы-потомки DBStmt

Потомки класса DBStmt включают в себя только методы, характерные для каждого вида операторов. Конструкторы данных классов создают заданные типы запросов: непосредственно выполняемый (DBStatement), подготавливаемый (DBPreparedStatement), вызов процедуры на сервере (DBCallableStatement). Конструкторы и поля данных этих классов приведены ниже на примере DBPreparedStatement:

class DBPreparedStatement 
extends DBStmt {
 private java.sql.PreparedStatement 
Stmt=null;
 public DBStatement(java.sql.Connection 
Con,String SQL)
 {
 super(SQL);
 try
 {
 Stmt = Con.prepareStatement(SQLString);
 }
 catch (SQLWarning w)
 {
 DBError.setWarning(w);
 }
 catch(SQLException e )
 {
 DBError.setException(e);
 }
 }
 ...

 }

Методы executeQuery, executeUpdate, execute реализованы отдельно для каждого класса подобно приводимому примеру для класса DBStatement:

public int executeUpdate ( ) {
int i= 0;
try	{
 i = Stmt.executeUpdate(SQLString);
	}	
catch (SQLWarning w)
	{
	DBError.setWarning(w);
	}	
catch(SQLException e )
	{
	DBError.setException(e);
 i = 0;
	}		
return i;
}

Естественно, каждый класс JDBC, выполняющий SQL-запросы (непосредственно выполняемые, подготавливаемые, вызовы процедур), имеет методы, характерные только для него. Для каждого ответственного за выполнение запросов класса библиотеки эти методы названы соответственно методам классов JDBC, например для DBPreparedStatement:

public void setInt (int 
ParamNumber, int ParamValue) {
try	{
 Stmt.setInt(ParamNumber,ParamValue);
	}	
catch (SQLWarning w)
	{
	DBError.setWarning(w);
	}	
catch(SQLException e )
	{
	DBError.setException(e);
	}		
}

За рассмотрением классов выполнения запросов читатели могли забыть о том, что с ними будет работать класс DBExchange, о методе которого prepareStatements упоминалось выше. Приведем простой пример:

//работа с телефонной книгой
class DBExchange extends DBConnection {
 ...
 private DBPreparedStatement 
ReceivePhones = null;
 private DBPreparedStatement 
InsertPhones = null;
 private DBPreparedStatement 
UpdatePhones = null;
 private DBPreparedStatement 
DeletePhones = null;
 ...
 private boolean prepareStatemens()
 {
 String S = «SELECT NAME,PHONE FROM 
PHONES WHERE ... «;
 ReceivePhones = new DBPreparedStatement(S);
 addErrors(ReceivePhones);
 S = «INSERT INTO PHONES VALUES ?,?»;
 InsertPhones = new DBPreparedStatement(S);
 addErrors(InsertPhones);
 //Инициализация update и delete 
столь же проста
 ...
UpdatePhones = ...
DeletePhones = ...
...
 if(true == DBError.errorsArePresent()) 
return false;
 return(true);
 }
 ...
 private void addErrors(DBStmt)
 {
 DBError.addErrorInfo(DBStmt
.getErrors());
 }
}

Четырем SQL-операторам в классе DBExchange соответствуют четыре public-метода, работающие с ними. Простейший, хотя и не лучший (но, как правило, первым приходящий в голову) способ их реализации приведен ниже:

public int insertPhones(String 
Name,String Phone)
{
int i;
InsertPhones.clearErrors();
InsertPhones.setString(1,Name);
InsertPhones.setString(2,Phone);
i = InsertPhones.executeUpdate();
if(i == 0) addErrors(InsertPhones);
return(i);
}

Попробуем реализовать подобным способом метод receivePhones, и посмотрим, что получится в результате:

...
public boolean receivePhones(int Mode)
{
if(Mode == DB_GET_FIRST)
 {
 ReceivePhones.clearErrors();
 ReceivePhones.executeQuery();
 addErrors(ReceivePhones);
 if(DBError.errorsArePresent()) 
return(false);
 }
if(DBError.errorsArePresent()) 
return(false);
if(false == ReceivePhones.next()) 
return false;

// Как передать в приложение 
множественные результаты?
? = ReceivePhones.getString(1);
? = ReceivePhones.getString(2);

addErrors(ReceivePhones);
if(DBError.errorsArePresent()) 
return(false);
return true;
}

В качестве вопросительных знаков могли бы выступать дополнительные поля, описанные в классе DBExchange. Однако сложные запросы, возвращающие десятки параметров, — не редкость, а если подобных запросов много, класс будет вовсе замусорен паразитными полями. Вдобавок, если описать эти поля как public, намного увеличится вероятность ошибок, а при описании private нужен метод get....() на каждое поле. Каким же образом организовать прием-передачу большого числа параметров? Выход в написании классов, подобных приведенному ниже:

class DBPhoneInfo {
private String Name=null;
private String Phone=null;
...
}

Читателям не составит труда написать для данного класса методы get и set. Взаимодействие основного приложения с классом DBExchange происходит при посредничестве класса DBPhoneInfo (см. схему 1).

Cхема 1

Из нее видно, что класс DBPhoneInfo служит как передаточным звеном между приложением и классом DBExchange, так и основным средством приема, хранения и передачи данных о строках телефонной книги между различными частями приложения, как-то: функции отображения телефона на экране, добавления нового телефона, исправления и удаления строк в телефонной книге и т. п. Далее класс, выполняющий перечисленные выше функции, будет называться передаточным. Читатели могут заметить, что программы, которые им нужно писать, намного сложнее, и правильный состав полей передаточного класса не всегда удастся подобрать с первого раза. Общие рекомендации для простых приложений, использующих только экранное отображение (без печати различных форм и прочих способов представления и передачи данных), таковы: выбор полей данного класса должен начинаться от экранных форм приложения, т. е. все, что должно отображаться на экранах, посвященных одному объекту (строке в телефонной книге или любому другому), должно входить в этот класс. Только после этого следует писать запросы, возвращающие из таблиц необходимые данные, а затем реализовывать методы receive, insert и т. д. в классе DBExchange для этого объекта.

Если приложение использует несколько типов данных (например, телефонная и адресная книги), необходима реализация нескольких передаточных классов. Естественно, при использовании неэкранных методов отображения и передачи информации необходимо внести в передаточный класс все необходимые им поля. Главное — ничего не упустить при этом. Дополнительное преимущество использования подобных классов как средства приема данных от приложения — возможность реализации контроля значений вводимых пользователем данных, а также преобразование их в иные виды. Это полезно, например, для java.sql.Date. Если на экране присутствует дата, то в классе можно предусмотреть два следующих метода:

public void setDateInfo
(java.sql.Date D)
public void setDateInfo(String Date,
String Month,String Year)

Первый метод используется для работы с SQL-запросами в классе DBExchange, а второй — при вводе данных пользователем в приложении. В последнем случае данные приводятся к виду, необходимому для передачи в SQL-запрос. Таким образом, передаточный класс имеет следующие достоинства:

  • решается проблема передачи множественных параметров;
  • унифицируется хранение, прием и передача данных об объекте в приложении;
  • облегчается контроль вводимых пользователем данных;
  • обеспечивается автоматическое преобразование получаемых от пользователя данных к JDBC-видам.

Рассмотрим два примера использования передаточного класса DBPhoneInfo: первый — рассмотренная выше функция receivePhones класса DBExchange, второй — применение передаточного класса в приложении.

1

public boolean receivePhones
(DBPhoneInfo Phone,int Mode)
{
if(Mode == DB_GET_FIRST)
 {
 ReceivePhones.clearErrors();
 ReceivePhones.executeQuery();
 addErrors(ReceivePhones);
 if(DBError.errorsArePresent()) 
return(false);
 }
if(false == ReceivePhones.next()) 
return false;
addErrors(ReceivePhones);
if(DBError.errorsArePresent()) 
return(false);

Phone.setName(ReceivePhones
.getString(1));
Phone.setPhone(ReceivePhones
.getString(2));

addErrors(ReceivePhones);
if(DBError.errorsArePresent()) 
return(false);
return true;
}

2

class Sample {
...
private DBPhoneInfo Phones = 
new DBPhoneInfo();
private DBExchange DBExch = 
new DBExchange();
...
DBExch.connect(Url,Userid,Password);
...
private void getNextPhone()
{
boolean res = false;
clearPhonesDisplay();
res = DBExch.receivePhones
(Phone,DB_GET_NEXT);
if(true == checkDBError())
 // Сообщение об ошибке
 {
 errorHandle();
 return;
 }
if(res == false) return;
showPhonesDisplay(Phone);
return;
}
...
private boolean checkDBError()
{
return((DBExch.getErrors())
.errorsArePresent());
}
...
private void clearPhonesDisplay()
{
getNameLabel().setText(«-»);
getPhoneLabel().setText(«-»);
}
...
private void showPhonesDisplay
(DBPhoneInfo Phone)
{
getNameLabel().setText(Phone
.getName());
getPhoneLabel().setText(Phone
.getPhone());
}
...
}

Приведенные примеры полностью соответствуют схеме 1, хотя она иллюстрирует лишь частный случай работы с телефонной книгой. Более полная иерархия классов библиотеки для работы с БД через JDBC и ее взаимодействие с приложением показаны на схеме 2.

Cхема 2

В заключение автор выражает надежду, что реализация и дальнейшая оптимизация под свои нужды описанной библиотеки поможет в изучении интерфейса JDBC. Конечно, большой объем подготовительной работы по написанию библиотеки может смутить любого, но все, кто знаком с объектно-ориентированным программированием, подтвердят: начальные усилия по реализации подобной иерархии классов лишь одноразовые. После завершения разработки библиотеки применение Java DataBase Connectivity станет еще легче и проще.

Литература

1. Р. Синор, М. О.Стегман. Использование ODBC для доступа к базам данных. М.: Бином. 1995.

2. Дж. Вебер. Технология Java в подлиннике. СПб.: BHV 1997.

ОБ АВТОРЕ

Алексей Федорович Костылев — менеджер отдела процессинга и технического обеспечения Альфа-Банка.


*Можно присвоить значение null и в процессе описания полей, например: private SQLException Excpt = null; (Прим. ред.)