Запись в файл 3 страница

Чтобы проиллюстрировать указанные выше различные методы, допустим, что имеется приложение, используемое для отображения данных служащего (фамилию, имя, оклад) по номеру его социального страхования. Это можно осуществить с использованием интерфейса Statement или PreparedStatement. В приведенном ниже коде показано непосредственное выполнение операторов SQL с помощью интерфейса Statement:

// пусть txtSSN - это ссылка на поле ввода класса java.awt.TextField, который воспринимает

// номер социального страхования в качестве вводимых пользователем данных

String sqlQuery="select LAST, FIRST, SALARY from PAYROLL where SSN="+txtSSN.getText();

// текст запроса

java.sql.Statement stmt=conn.createStatement();

// создание запроса

java.sql.ResultSet rset=stmt.executeQuery(sqlQuery);

// выполнение запроса и получения результата запроса

// использование результата запроса - объекта rset

. . . . . . .

stmt.close();

// освобождение ресурсов запроса

Этот же запрос может быть параметризирован и выполнен следующим образом:

// комментарии - аналогичные

String sqlQuery="select LAST, FIRST, SALARY from PAYROLL where SSN=?";

// текст запроса

java.sql.PreparedStatement pstmt=conn.prepareStatement(sqlQuery);

// создание запроса

pstmt.setLong(1,java.lang.Long.parseLong(txtSSN.getText()));

// установка параметра

java.sql.ResultSet rset=pstmt.executeQuery();

// выполнение запроса

// использование результата запроса - объекта rset

. . . . . . .

Интерфейс PreparedStatement позволяет повторно выполнять этот запрос с новым параметром любое число раз, создавая новый набор ResultSet:

pstmt.setLong(1,java.lang.Long.parseLong(txtSSN.getText()));

// установка параметра

java.sql.ResultSet rset=pstmt.executeQuery();

// выполнение запроса

// использование результата запроса - объекта rset

. . . . . . .

Когда запрос pstmt уже не нужен, его следует закрыть, освобождая связанные с ним ресурсы:

pstmt.close(); // освобождение ресурсов запроса

Метод setLong() является одним из методов интерфейса PreparedStatement, используемых для задания значений параметров. Каждый из этих методов воспринимает индекс параметров с отсчетом от единицы и значение параметра. Например, метод setString() воспринимает индекс параметров и класс java.lang.String.

К сожалению, не все драйверы поддерживают все типы данных интерфейса JDBC и не одинаково выполняют подготовленные операторы SQL. Некоторые драйверы могут задержать интерпретацию и установку параметров до тех пор, пока не начнется выполнение запроса. Это позволяет повысить производительность, однако может нарушить обработку исключений по причине задержки активизации некоторых исключений. Например, исключение SQLException может быть активизировано в том случае, если один из методов set() вызывается с типом данных, несовместимым с соответствующим столбцом базы данных. Если интерпретация и установка параметров задержаны, то это же исключение не будет активизировано до тех пор, пока не будет вызван метод executeQuery().

1.5 Выборка результатов

Рассмотрим теперь, как выбрать результаты запроса, если они выдаются в виде набора java.sql.ResultSet. Интерфейс ResultSet, входящий в состав интерфейса JDBC, как подразумевает его наименование, специально предназначен для выборки результатов.

Интерфейс Statement, входящий в состав интерфейса JDBC, представляет собой основной интерфейс взаимодействия с базой данных. Он расширяется за счет интерфейсов CallableStatement и PreparedStatement. Интерфейс ResultSet можно получить через интерфейс Statement или PreparedStatement (интерфейс CallableStatement в основном используется для процедурных расширений и рассматривается в последующих разделах.).

Интерфейс Statement следует использовать для создания интерфейса ResultSet только в том случае, если запрос SQL является статичным. А для параметризованных запросов SQL или часто выполняемых запросов следует использовать интерфейс PreparedStatement. В том случае, когда используется интерфейс PreparedStatement, имеется возможность выполнить предкомпиляцию операторов SQL и привязать план доступа к конкретной базе данных. Интерфейс PreparedStatement содержит методы установки значений параметров до выполнения. Это дает возможность многократно выполнять предварительно скомпилированные операторы SQL с разными значениями параметров, что может привести к существенному повышению производительности, в особенности при сложных запросах.

Независимо от того, осуществляется ли непосредственное (с помощью интерфейса Statement) или подготовленное (с помощью интерфейса PreparedStatement) выполнение операторов SQL, возвращаемый интерфейс ResultSet оказывается одним и тем же. Доступ к строке осуществляется с помощью метода next() интерфейса ResultSet. Этот метод возвращает логическое значение, которое является истинным до тех пор, пока не будут считаны все строки. Кроме того, он может активизировать исключение SQLException, если возникнет ошибка.

После того как курсор будет установлен на строку, значения столбцов выбираются с помощью одного из методов get() интерфейса ResultSet (getStringO, getInt() и др.). Каждый из этих методов воспринимает для выборки индекс столбца с отсчетом от единицы или имя столбца. Следующий код служит расширением примера запроса, представленного в приведенных выше фрагментах кода:

String lastName="";

String firstName="";

float salary=0;

while(rset.next())// цикл по строкам результата запроса

{

// получение значений столбцов результата запроса

lastName=rset.getString(1); // или lastName=rset.getString("LAST");

firstName=rset.getString(2); // или firstName=rset.getString("FIRST");

salary=rset.getFloat(3); // или salary=rset.getFloat("SALARY");

// выполнить что-либо по использованию полученных значений

. . . . . . . . . . .

}

rset.close(); // освобождение результата запроса

Рекомендуется закрывать результирующий набор, как только в нем отпадает необходимость. Метод close() применяется также в интерфейсах Statement и PreparedStatement. Однако, возможно, потребуется оставить интерфейсы Statement открытыми для последующих выполнений операторов SQL, поскольку их закрытие освобождает все связанные с ними ресурсы. Если ссылка на интерфейс Statement становится недействительной, метод close() вызывается автоматически в процессе сборки мусора. Вызов метода close() до определения недействительности ссыкли гарантирует немедленное освобождение внешних ресурсов.

1.6 Применение интерфейса ResultSetMetaData

Интерфейс java.sql.ResultSetMetaData используется для получения сведений о результирующем наборе и его столбцах во время выполнения. Этот интерфейс может стать эффективным инструментальным средством при разработке многократно используемых компонентов отображения, когда характер результирующего набора не известен на этапе проектирования. Интерфейс ResultSetMetaData может быть получен из любой допустимой ссылки на интерфейс ResultSet:

java.sql.ResultSetMetadata rsetInfo=rset.getMetadata(); // получение информации о результате запроса

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

int numCols=rsetInfo.getColumnCount(); // количество столбцов результата запроса

String colNames[]=new String[numCols];

int colSizes[]=new int[numCols];

int colTypes[]=new int[numCols];

for(int i=0; i<numCols; i++) // цикл по столбцам результата запроса

{

// получение информации о столбцах результата запроса

colNames[i]=rsetInfo.getColumnLabel(i+1); // индексы столбцов в JDBC начинаются с 1

colSizes[i]=rsetInfo.getColumnDisplaySize(i+1);

colTypes[i]=rsetInfo.getColumnType(i+1);

}

МетодgetColumnType() возвращает целое значение, представляющее собой тип данных SQL. Константы типа данных SQL определены в классе java.sql.Types. В контексте приведенного выше примера этой информацией можно воспользоваться для определения способа отображения столбца с выравниванием по левому или по правому краю. Следующий фрагмент кода служит расширением приведенного выше примера для выборки значений столбцов в каждой строке:

int numCols=rsetInfo.getColumnCount();

String colValues[]=new String[numCols];

int i=0;

while(rset.next()); // цикл по строкам результата запроса

{

for(int i=0; i<numCols; i++) // цикл по столбцам результата запроса

{

colValues[i]=rset.getString(rsetInfo.getColumnLabel(i+1)); // получение значения столбца результата запроса

// выполнить что-либо по использованию полученного значения этого столбца

. . . . . . . . . . .

}

}

В приведенном выше фрагменте кода метод getString() используется для получения значений всех столбцов. Этот метод обычно можно использовать независимо от типа данных столбца, поскольку в большинстве драйверов поддерживается преобразование любого типа данных в его строковое представление. С другой стороны, сведения о типе данных могут быть использованы в приложении для сохранения значений столбцов в более подходящей структуре с последующим применением соответствующего метода get() к каждому столбцу в зависимости от его типа данных. Необходимость в этом может возникнуть, если данные используются в математических расчетах, однако нередко более удобной оказывается выборка значений столбцов в виде строк в целях их последующего отображения.

При использовании метода getlnt() и других числовых методов может потребоваться специальная обработка пустых значений. Так, метод wasNull() может быть использован для того, чтобы определить, является ли пустым значение последнего считанного столбца:

double comm=rset.getDouble("COMM");

if(rset.wasNull())

// выполнить обработку пустого значения (например, установить соответствующий признак)

. . . . . . . . . .

1.7 Доступ к хранимым функциям и процедурам

Доступ к хранимым процедурам и функциям выполняется с помощью интерфейса java.sql.CallableStatement и синтаксиса управляющих последовательностей SQL. Интерфейс CallableStatement расширяет интерфейс PreparedStatement, который, в свою очередь, расширяет интерфейс Statement. Интерфейс CallableStatement существует для того, чтобы предоставлять дополнительные методы, специально предназначенные для вызова хранимых процедур и функций и обработки выходных параметров.

Синтаксис управляющих последовательностей SQL для вызова хранимой процедуры из интерфейса JDBC имеет следующую общую форму:

{call proc_name(arg1,arg2,…)}

А вызов функции, выполняемой через интерфейс JDBC, имеет следующую общую форму:

{?=call func_name(arg1,arg2,…)}

Ссылку на объект CallableStatement можно получить непосредственно из интерфейса Connection, почти так же как ссылку на объект PreparedStatement. Хотя объекты PreparedStatement можно получить с помощью метода preparedStatement(), объекты CallableStatement можно получить с помощью метода ргерагеСаll(). Допустим, что существует хранимая процедура Oracle c именем delete_rows,предназначенная для удаления из некоторой таблицы тех записей, в которых одно из полей больше одного числа, но меньше другого (оба эти числа передаются через параметры). Код JDBC для выполнения операции вставки данных может иметь следующий вид:

int min=10; String max="20";

String querySql="{call delete_rows (?,?)}";// текст запроса

CallableStatement cstmt = conn.prepareCall(query,Sql); // создание запроса

cstmt.setInt(1,min); // установка значения первого аргумента процедуры

cstmt.setString(2,max); // установка значения второго аргумента процедуры

cstmt.executeUpdate(); // выполнение запроса

conn.commit(); // фиксация изменений

cstmt.close(); // освобождение ресурсов запроса

В действительности эту процедуру можно было бы выполнить с помощью интерфейса PreparedStatement, а не интерфейса CallableStatement, поскольку в этом случае нужны только те методы класса CallableStatement, которые унаследованы от класса PreparedStatement. Однако рекомендуется использовать интерфейс CallableStatement для выполнения процедур, что позволяет свести к минимуму влияние изменений в базовой процедуре. Интерфейс CallableStatement необходимо использовать для доступа к выходным параметрам, которые рассматриваются в следующем разделе.

1.8 Применение выходных параметров

Нередко в процедурах и функциях Oracle полезным оказывается применение выходных параметров. Интерфейс CallableStatement, входящий в состав интерфейса JDBC, предоставляет методы выборки этих значений. Допустим, что операция удаления данных из таблицы delete_rows, рассмотренная в предыдущем разделе, написана в виде пакетной функции и что это функция возвращает количество удаленных строк. Для примера допустим далее, что значение второй входной параметр является также и выходным параметром этой функции (пусть, например, там передается начальное количество строк таблицы, из которой производится удаление). В приведенном ниже фрагменте кода показан вызов этой функции и выборка выходных параметров:

int min=10, max=20;

String querySql="{?= call delete_rows (?,?)}";// текст запроса

CallableStatement cstmt = conn.prepareCall(query,Sql); // создание запроса

cstmt.registerOutParameter(1,java.sql.Types.INTEGER);

// регистрация типа возвращаемого функцией значения

cstmt.setInt(2,min); // установка значения первого аргумента

cstmt.setInt(3,max); // установка значения второго аргумента

cstmt.registerOutParameter(3,java.sql.Types.INTEGER);

// регистрация типа значения, которое возвращается через второй аргумент функции

cstmt.executeUpdate(); // выполнение запроса

int del=cstmt.getInt(1); // получение возвращаемого функцией значения

int rows=cstmt.getInt(3); // получение значения, возвращаемого во втором аргументе функции

conn.commit(); // фиксация изменений

cstmt.close(); // освобождение ресурсов запроса

Что же касается интерфейса JDBC, то функция Oracle является процедурой, имеющей, по меньшей мере, один выходной параметр. Не существует специальной обработки возвращаемого функцией значения. Аргументами метода registerOutParameter() являются индекс параметра с отсчетом от единицы и тип данных SQL этого параметра. Значения выходных параметров выбираются после выполнения с помощью соответствующего метода get() (getStringQ, getF1oat() и др.). Используемый метод get() должен быть совместим с типом данных, передаваемым методу registerOutParameterO. Методы get() класса CallableStatement аналогичны методам get() класса ResultSet, которые воспринимают индекс столбца в качестве своего аргумента. Кроме того, подобно соответствующему методу класса ResultSet, метод wasNulI() позволяет определить, был ли пустым последний считанный выходной параметр.

Входные и выходные параметры INOUT должны быть заданы и зарегистрированы. Тип данных SQL, передаваемый методу registerOutParameter(), должен соответствовать используемому методу set(). Параметр INOUT может быть задан и зарегистрирован так, как, например, в следующем примере:

cstmt.setInt(3,max);

cstmt.registerOutParameter(3,java.sql.Types.INTEGER);

2. Обработка исключений JDBC

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

SQLException

SQLWarning

DataTruncation

Исключение SQLException активизируется методами интерфейса JDBC в том случае, если в базе данных возникает ошибка. Существует ряд методов, которые могут быть вызваны для выборки информации об ошибке:

· Метод int getErrorCodeO возвращает код ошибки конкретной базы данных

· Метод String getSQLState() возвращает состояние X/Open SQLState, связанное с ошибкой

· Метод String getMessage() наследуется из класса java.lang.Exception (возвращает описание ошибки)

Исключения JDBC могут быть связаны в цепочку, поэтому, если во время выполнения одной операции возникает более одной ошибки, информация обо всех ошибках сохраняется. Метод getNextException() возвращает ссылку на следующее исключение в цепочке или пустое значение, если исключений больше нет.

SQLWarning представляет собой такое исключение, которое не активизируется, но связывается в цепочку с конкретным объектом ResultSet или объектом оператора, к которому оно применяется. Как и в случае с исключениями SQLException, в цепочку может быть связано более одного исключения SQLWarning. Метод getNextWarning() класса SQLWarning возвращает следующее предупреждение в цепочке или пустое значение, если предупреждений больше нет. Например, при обработке результирующего набора в приложении наличие предупреждений можно проверить с помощью метода getWarnings() следующим образом:

java.sql.SQLWarning warn=rset.getWarnings();

while(warn!=null)

{

// сделать что-нибудь с предупреждением

. . . . .. . . . .

warn=warn.getNextWarning();

}

Исключение DataTruncation является особым случаем исключения SQLWarning, которое применяется к значениям столбцов и параметров. Исключение DataTruncation может быть связано в цепочку с объектом результирующего набора, однако оно активизируется при его применении к объекту оператора. Оно предоставляет дополнительные методы определения индекса столбца или параметра, к которому оно применяется, фактической и ожидаемой длины и т.д. При связывании исключения DataTruncation в цепочку с результирующим набором отличить его от других предупреждений можно на основании значения "01004" его состояния SQLState. Приведенный выше фрагмент кода можно расширить для проверки усечения данных:

int colNum, javaLen, dbLen;

java.sql.SQLWarning warn=rset.getWarnings();

while(warn!=null)

{

if(warn.getSQLState()=="01004")

{

colName=((DataTruncation)warn).getIndex();

javaLen=((DataTruncation)warn).getTransferSize();

dbLen=((DataTruncation)warn).getDataSize();

// сделать что-нибудь с этой информацией

. . . . .. . . . .

}

else

{

// это другое исключение java.sql.SQLWarning

. . . . .. . . . .

}

warn=warn.getNextWarning();

}

Методы getTransferSize() и getDataSize() применяются к длине столбца или параметра интерфейса JDBC, а также столбца или параметра базы данных. Исключение DataTruncation, скорее всего, будет активизировано тогда, когда значение параметра в результате окажется слишком длинным. В этом случае оно не связывается в цепочку в виде предупреждения, но активизируется в виде исключения, которое должно быть перехвачено:

int parmName, javaLen, dbLen;

try

{

cstmt.executeUpdate();

}

catch(SQLException e)

{

while(e!=null)

{

if(e.getSQLState()=="01004")

{

parmName=((DataTruncation)warn).getIndex();

javaLen=((DataTruncation)warn).getTransferSize();

dbLen=((DataTruncation)warn).getDataSize();

// сделать что-нибудь с этой информацией

. . . . .. . . . .

}

else

{

// это другое исключение SQLException

. . . . .. . . . .

}

e=e.getNextWarning();

}

}

Исключение DataTruncation может быть перехвачено как исключение SQLException, поскольку оно происходит от исключения SQLException. А когда оно перехватывается как исключение SQLException, то последнее необходимо явно привести к исключению DataTruncation, прежде чем получить доступ к методам этого интерфейса.

Обработка перехваченного исключения SQLException в значительной степени зависит от конкретных потребностей приложения. В некоторых случаях может оказаться полезной просто повторная активизация перехваченного исключения или активизация исключения специально для конкретного приложения, чтобы его можно было обработать на более высоком уровне. При активизации исключения из блока catch следует рассмотреть ряд вопросов. Для тех читателей, которые знакомы с языком Java, очевидным является первое положение: активизация исключений из вложенных блоков try — далеко не самая удачная идея. Рассмотрим следующий пример, в котором активизируется исключение, являющееся расширением класса java.lang.Exception и определенное в приложении как DBException:

try

{

// некоторые операции получения объектов Java, присвоения значений и т.д.

. . . . . . . . . . .

try

{

// некоторые вызовы интерфейса JDBC

}

catch(SQLException sqle)

{

// выполнить очистку и активизировать исключение, чтобы его можно было активизировать на более высоком уровне

. . . . . . .

throw new DBException(sqle.getMessage(),sqle.getErrorCode());

}

}

catch(java.lang.Exception e)

{

// выполнить что-нибудь

. . . . . . .

}

Новое исключение DBException, активизированное из внутреннего блока, перехватывается во внешнем блоке, поскольку исключение DBException является потомком класса java.lang.Exception. Добиться требуемого результата можно с помощью нескольких перехватов для внешнего блока try:

try

{

// некоторые операции получения объектов Java, присвоения значений и т.д.

// некоторые вызовы интерфейса JDBC

. . . . . . . . . . .

}

catch(SQLException sqle)

{

// выполнить очистку и активизировать исключение, чтобы его можно было активизировать на более высоком уровне

. . . . . . .

throw new DBException(sqle.getMessage(),sqle.getErrorCode());

}

catch(java.lang.Exception e)

{

// выполнить что-нибудь

. . . . . . .

}

Исключения должны перехватываться от низшего к высшему уровню иерархии. Если исключение java.lang.Exception перехватывается до исключения DBException, то блок DBException не будет достигнут. В предыдущем примере информация из первого перехваченного исключения SQLException копируется в новое исключение DBException. Если же несколько исключений связываются в цепочку, тогда эта информация не сохраняется. Цепочка может быть сохранена в том случае, если исключение DBException расширяет класс java.lang.Exception с помощью метода SetNextException():

catch(Exception sqle)

{

DBException first=new DBException(sqle.getMessage(),sqle.getErrorCode());

sqle=sqle.getNextException();

while(sqle!=null)

{

first.setNextException(sqle);

}

throw first;

}

Для перехвата исключения SQLException нередко требуются дополнительные действия, особенно в контексте транзакции. Допустим, что в приложении определен метод, который выполняет вставку строки в три отдельные таблицы в течение одной транзакции. Если при этом любая из операций вставки потерпит неудачу, должен быть осуществлен откат всей транзакции. В этом случае следует выдать команду отката в обработчике исключений. Это лишь один простой пример множества ситуаций, в которых исключение SQLException требует выполнения очистки. Следует иметь в виду, что в результате вызовов интерфейса JDBC распределяются внешние ресурсы и изменяется состояние базы данных, и поэтому обработчики исключений нужно проектировать соответствующим образом.

3. Отладка приложений JDBC

Отладка приложения JDBC оказывается более сложной, чем отладка простого апплета Java, сервлета или автономного приложения, поскольку она включает в себя дополнительные уровни программного обеспечения для взаимодействия с базой данных. Дело еще более усложняется тем, что часть драйвера JDBC может быть реализована в виде двоичной библиотеки, которая недоступна для отладчика JDB, являющегося стандартным инструментальным средством отладки, входящим в состав пакета JDK.

К счастью, в интерфейсе JDBC предусмотрен механизм просмотра операторов, обрабатываемых драйвером, а также информации, возвращаемой из базы данных. Для регистрации информации драйвера в файле или в стандартном выводе можно воспользоваться методом setLogStream() класса DriverManager:

DriverManager.setLogStream(System.out);

Метод setLogStream() класса DriverManager воспринимает любой объект класса java.io.PrintStream. Предоставляемая информация включает вызываемый метод JDBC, значения параметров и возвращаемые данные. В некоторых случаях предоставляется дополнительная информация для конкретного драйвера, которая может оказаться еще более полезной. Например, драйверы Oracle могут регистрировать базовые функции, параметры и возвращаемые значения интерфейса OCI, обрабатываемые драйвером в результате вызова интерфейса JDBC. Зарегистрированную таким образом информацию можно использовать для определения вызовов вне заданной последовательности, синтаксических ошибок SQL и других программных ошибок. В некоторых случаях она позволяет также обнаружить недокументированные средства драйвера.

Кроме того, можно воспользоваться инструментальным средством JDBCTest только на языке Java, разработанным компаниями JavaSoft и Intersolv для проверки драйверов JDBC. Оно содержит графический интерфейс и систему меню для выполнения вызовов API-интерфейса JDBC. Вызовы интерфейса JDBC, которые приводят к ошибкам в приложениях, могут быть выполнены через этот интерфейс, чтобы определить, была ли данная ошибка вызвана драйвером или кодом приложения. Это инструментальное средство проверки может оказаться особенно полезным при выборе одного или нескольких драйверов для проектирования и разработки приложения. Например, нельзя заранее сказать, что драйвер OracleS будет поддерживать абстрактные типы данных и коллекции OracleS с помощью методов set0bject() и get0bject(). Эти весьма специфические для RDBMS средства не обязательно должны быть полностью совместимы с интерфейсом JDBC.

К сожалению, средство JDBCTest может не работать с драйверами только на языке Java. Большинству драйверов только на языке Java требуется код для установления взаимодействия с серверным компонентом перед регистрацией драйвера и установлением соединения. Многие из этих драйверов имеют нестандартные реализации общепринятых методов, превращая тем самым JDBCTest просто в непригодное для указанных выше целей инструментальное средство. Компания JavaSoft предоставляет более автоматизированное инструментальное средство проверки (программу проверки интерфейса JDBC), а также ряд тестовых комплектов, используемых вместе с этой программой проверки. Это инструментальное средство является более гибким, поскольку оно работает под управлением файла конфигурации и позволяет выполнять разработку специализированных проверочных комплектов с минимальными усилиями. Надлежащая проверка на предшествующем проектированию этапе даст возможность заранее обнаружить присущие конкретному драйверу ограничения, что позволит избежать многих проблем в процессе разработки.

4. Сервлет, работающий с информацией из базы данных

Проверить работу сервлета example_db (исходный текст см. ниже), объяснить, что происходит в блоке try-catch. В заголовках уровня 2 генерируемого html-файла указать, какое действие происходит в каждой секции (ненужное вычеркнуть).

/*------------- Пример 1. Файл example_db.java -------------*/

package examples; // пакет для сервлета-примера

// способы вызова клиентом класса example.example_db через строку адреса окна браузера