Магазин портативной техники "Портатив"
:: на главную
Java > Повышение производительности с помощью JDBC (часть 1)


Модели процесса получения данных с Java-сервера с помощью JDBC
Содержание

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

Современные прикладные системы в большой степени зависят от поддержки механизма хранения серверных данных, использующего для этого одну и более баз данных. Наиболее удобным способом обращения серверного Java-приложения к базе данных является использование иерархии классов пакета java.sql. Тем не менее, не смотря на качественный дизайн большинства JDBC-классов, я заметил, что корпорации, создающие системы получения данных на Java, обычно используют не столько производительные, сколько упрощённые модели работы JDBC. Ознакомившись с предлагаемыми в этой статье правилами расширения JDBC API, вы сможете осознать и преимущества, которые он может вам дать. Модели, приведенные в этой статье, могут быть реализованы с JDBC версии 1.22 и 2.0

Наиболее распространённые сценарии работы
Двумя наиболее распространенными сценариями получения данных являются стандартные сценарии ResultSet и CallableStatement, которые используются для создания серверных приложений, работающих с базами данных. Вы вполне можете воспользоваться любым из них для работы с любой базой, поскольку имеющийся в нашем распоряжении JDBC API поддерживает и тот и другой. Для специфических целей, определённых в этой статье, давайте допустим, что мы работаем с мощной промышленной базой данных, способной управлять компилируемыми для базы данных процедурами хранения.

Таблица 1. Описание сценариев.

Сценарий
Описание
Диаграмма сценария
ResultSet
Создать Statement (или Prepared Statement) из открытой Connection с базой данных. Передать SQL-запрос в Statement и получить ResultSet с логическим отображением из базы данных.

Callable Statement
Создать Callable Statement из открытой Connection с базой данных. Передать процедурный SQL-вызов (выполнив процедуру хранения) в Callable Statement и получить из базы данных примитивный тип или строку.


А теперь взгляните на базовый код стандартного ResultSet-сценария:

// Database connection supposedly
// already made.
Connection conn;
Statement aStatement = null;
ResultSet aResultSet = null;

try
{
   // Create the database statement.
   aStatement = conn.createStatement();

   // Ship the question to the database
   aResultSet = aStatement.executeQuery("<some_SQL_query>");

   // Handle the result
   while(aResultSet.next())
   {
         // ...
   }
}
catch(Exception ex)
{
   // Log an error message
   System.err.println("Database communication error: " + ex);
}

А это базовый код стандартного CallableStatement-сценария:

// Database connection supposedly
// already made.
Connection conn;
CallableStatement aStatement = null;

try
{
   // Create the database callable statement.
   // For example, assume that the "<Some_SQL_Procedure_call>"
   // is something like "{call calculate_interest_rate(?)}"
   // where the '?' denotes an output parameter.
   aStatement = conn.prepareCall("<Some_SQL_Procedure_call>");

   // Register a primitive output parameter, in this
   // example case of SQL type INTEGER. All type constants
   // can be found in the java.sql.Types class.
   aStatement.registerOutParameter(1, Types.INTEGER);

   // Call the procedure in the database
   aStatement.executeUpdate();

   // Handle the output
   int valueReceived = aStatement.getInt(1);
}
catch(Exception ex)
{
   // Log an error message
   System.err.println("Database communication error: " + ex);
}

Интерфейс CallableStatement обеспечивает большое многообразие методов типа getXXX. В результате чего он имеет возможность получать от процедур хранения базы данных параметры различных типов. Зарегистрированные параметры должны быть задекларированы в процедуре хранения в виде параметров OUT, если вы хотите, чтобы getXXX мог их получать.

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

Таблица 2. Производственные факторы, влияющие на стандартные сценарии из Таблицы 1.

Вопрос
Ответ
Какие из разновидностей JDBC-запросов влияют на потерю производительности обоих сценариев? SQL-запрос (возвращающий многочисленные ряды и столбцы), который вызывается несколько раз при различных поисковых условиях и делает процесс SQL-компиляции сложным и громоздким. Другими словами, это архитип для создания процедуры хранения для возвращения ResultSet.
Почему добиться повышения скорости можно с помощью создания процедуры хранения базы данных, а не отправления SQL-запроса к процессору базы данных? Хотя вы не можете точно знать, как драйверный класс базы данных реализует вызовы своих методов, всё же результат обрабатывается быстрее, поскольку мы получаем его в непосредственной близости от процессора базы данных. В принципе, узкие места могут появляться везде, где используется сетевое взаимодействие или внутрипроцессовые соединения. Поэтому самым быстрым способом собрать двухмерный ResultSet (т.е. такой, который содержит данные из нескольких таблиц и, возможно, их значения) является создание для вызова процедуры хранения.

Таким образом, наиболее простым и быстрым способом сборки двухмерных ResultSets (содержащих данные из нескольких таблиц и, возможно, их значения) служит создание процедуры хранения базы данных для осуществления вызовов. С точки зрения дизайна, возможности системы формировать пакеты данных значительно возрастают, если за именем процедуры скрыты важные механизмы работы базы данных. Используя преимущества формирования пакетов данных, имеющиеся в объектно-ориентированном программировании, вы можете повысить гибкость системы, поскольку у вас появляется возможность реструктурировать данные базы данных, не коим образом не затрагивая Java-приложения для сервера. Говоря иначе, отделение физического отображения базы данных от её логического отображения, как это видит приложение, делает систему более быстрой и удобной для дальнейшего совершенствования. Повышение уровня производительности находящейся на сервере базы данных может быть достигнуто с помощью применения комбинации двух уже известных нам сценариев, как это показано в Таблице 3.

Таблица 3. Комбинированный сценарий ResultSet CallableStatement

Сценарий
Описание
Диаграмма сценария
ResultSet Callable Statement Создать CallableStatement из открытой Connection с базой данных. Передать вызов SQL-процедуры (выполнив процедуру хранения базы данных) в CallableStatement и получить от базы данных ResultSet.


Хотя на первый взгляд реализация CallableStatement-сценарий на Java кажется не сложной, более менее опытный разработчик сразу задумается над головной болью связанной с различиями между базами данных и JDBC-драйверами для баз данных. Несмотря на тот факт, что JDBC задумана как технология независимая от типа платформы или базы данных, специфические особенности каждой из баз данных бросаются в глаза, не только тогда когда мы определяем процедуру хранения, но и тогда когда пытаемся вернуть из неё ResultSet. Процессоры некоторых баз данных не умеют возвращать значения из процедур хранения. Поэтому значения, которые должны возвращаться в Java-приложение, должны вместо этого декларироваться как выходные параметры. Некоторые процессоры могут возвращать из процедур хранения только определённые типы (в основном, INTEGER, которые указывают статус кода ошибки вызова процедуры). Есть ещё такие типы, которые могут возвращать абсолютный JDBC-эквивалент ResultSet (в некоторых случаях это будет соответствовать типу CURSOR данной базы), либо в виде результата вызова процедуры хранения, либо в виде выходного параметра.

В зависимости от возможностей процессора выполнять обращения к процедурам хранения могут использоваться разные методы вызова, как это показано в Таблице 4.

Таблица 4. Методы вызова, использующиеся в разных базах данных.

Сценарий
Возможности базы данных
Описание
1
Процедуры хранения не могут возвращать значение, но могут возвращать выходной параметр. Прежде чем делать запрос, входной и выходной параметры должны быть установлены/зарегистрированы в CallableStatement.
2
Процедура хранения может возвращать значение, но только примитивного типа (как INTEGER код ошибки).
Процедура хранения может возвращать выходной параметр любого вида.
Если значение процедуры хранения, которое нужно вернуть, не того типа/типов, которые использует база, все параметры должны быть вначале установлены/зарегистрированы в CallableStatement.
3
Процедура хранения может возвращать значение любого типа. ResultSets могут возвращаться сами или как выходные параметры. Перед выполнением запроса должны быть установлены все входные параметры.
4
Процедура хранения может возвращать (многочисленные) имплицитные ResultSets без необходимости использовать выходные параметры или возвращаемые значения. (Многочисленные) ResultSets могут возвращаться имплицитно. Выходные параметры не требуются.

А теперь рассмотрим псевдокод базы данных, приведенный для описания каждого из сценариев. Подробное описание каждого из сценариев даётся в следующем разделе.

Таблица 5. Псевдокод базы данных для сценариев, перечисленных в Таблице 4.

Сценарий
Псевдокод базы данных
1
CREATE PROCEDURE <procedureName>
  (theResult CURSOR OUTPUT, <otherParameters>)
AS
 /* Gather data from all over database */
 /* .... */
END PROCEDURE
2
CREATE PROCEDURE <procedureName>
 ( theResult CURSOR OUTPUT, <otherParameters>)
AS
 /* Gather data from all over database */
 /* .... */
 RETURN <statusCode>
END PROCEDURE
3
CREATE PROCEDURE <procedureName>
  (<inputParameters>)RETURNING CURSOR
AS
 /* Gather data from all over database */
 /* .... */
 RETURN <aCursor>
END PROCEDURE
4
CREATE PROCEDURE <procedureName>
  (<inputParameters>)
AS
 /* Gather data from all over database */
 /* .... */
 SELECT .... /* Creates an output ResultSet */
END PROCEDURE

Структура вызовов JDBC (и соответствующие виды реализации серверных Java-приложений) во многом зависит от возможностей процессора базы данных. Если вдруг вам понадобится, чтобы процедура хранения возвращала объекты ResultSet, ситуация может значительно усложниться. В следующей статье я покажу типовой дизайн Java-приложения, с помощью которого можно будет снизить нагрузку на приложение в процессе получения от процедуры хранения многочисленных ResultSets. А пока что, давайте рассмотрим Java-приложение, работающее с базой данных по каждому из четырёх сценариев. В данный момент нас интересуют только выходные объекты ResultSet.

Базовый код JDBC для Сценария 1
Приведенный ниже код иллюстрирует реализацию нашего первого сценария для JDBC.

// Database connection supposedly
// already made.
Connection conn;
CallableStatement aStatement = null;

try
{
   // Create the database callable statement.
   // For example, assume that the "<Some_SQL_Procedure_call>"
   // is something like "{call calculate_living_costs(?)}"
   // where the '?' denotes an output parameter that should
   // be interpreted as a java.sql.ResultSet
   aStatement = conn.prepareCall("<Some_SQL_Procedure_call>");

   // Register a primitive output parameter
   // as a .... WHAT?
   //
   // Remember, no static final parameter that would indicate a ResultSet
   // or database CURSOR type exists in the java.sql.Types class.
   // The only standard option is to use the ResultSet.getObject method
   // to extract the CURSOR, then hope that it can be cast into a
   // java.sql.ResultSet.
   //
   // Thus, you have to consult the database JDBC driver documentation
   // to see how an output CURSOR/ResultSet should be registered.
   //
   // In most cases, the type info parameter sent in to the
   // registerOutParameter method is a type native to the
   // driver. The result: Non-standard type behavior...
   //
   // *Messy*
   //
   aStatement.registerOutParameter(1, MyJDBCDriverTypes.RESULTSET_TYPE);

   // Call the procedure in the database
   aStatement.execute();

   // Handle the output
   //
   // Unfortunately, there is no method getXXX that returns a
   // java.sql.ResultSet object in the java.sql.CallableStatement
   // interface.
   //
   // Thus, you have to consult the database JDBC driver documentation
   // to see what method you would call to interpret the outgoing
   // database CURSOR as a java.sql.ResultSet.
   //
   // Hopefully, you could extract a java Object, which could be
   // cast into a ResultSet.
   //
   ResultSet valueReceived = ((<SpecificDriverClass>) aStatement).getResultSet(1);

   // Handle the received ResultSet
   while(valueReceived.next())
   {
       // ...
   }    
}
catch(Exception ex)
{
   // Log an error message
   System.err.println("Database communication error: " + ex);
}

Наиболее очевидной проблемой данного подхода является зависимость кода от базы данных. Это происходит из-за того, что два вызова для регистрации выходящего параметра ResultSet и получения самих данных используют специфический для JDBC-драйвера код, поскольку в java.sql.Types и java.sql.CallableStatement нет прямой поддержки процесса обработки ResultSets в качестве выходных параметров.

Некоторые из JDBC-драйверов решают эту проблему, обрабатывая объект ResultSet как объект типа java.lang.Object. Таким образом, исходящий ResultSet может обрабатываться как объект, введённый в java.sql.ResultSet, прежде чем его начнут использовать для экстрактирования данных. Данный способ работы с ResultSets предполагает с помощью добавления метода getResultSet(int index) в java.sql.CallableStatement и константы java.sql.Types.RESULTSET можно улучшить JDBC API.

В качестве альтернативного варианта ResultSet можно экстрактировать с помощью метода Object getObject(int index), а затем привести его к ResultSet. Сверьтесь с документацией для драйвера, возможно ли это.

Базовый код JDBC для Сценария 2
Вообще-то, такая же проблема, характерная для нашего первого сценария, присуща и второму. Поэтому для того чтобы получить ResultSet от CallableStatement, использующего драйвер базы данных, которые не умеет обрабатывать возвращаемые значения типа CURSOR, так как для этого ему требуются вызовы специфических для драйвера методов (и иногда спецификации типов).

// Database connection supposedly
// already made.
Connection conn;
CallableStatement aStatement = null;

try
{
   // Create the database callable statement.
   // For example, assume that the "<Some_SQL_Procedure_call>"
   // is something like "{? = call calculate_living_costs(?)}"
   // where the '?' denotes an output parameter that should
   // be interpreted as a java.sql.ResultSet
   aStatement = conn.prepareCall("<Some_SQL_Procedure_call>");

   // Register a primitive output parameter
   // as a .... WHAT?
   //
   // Remember, no static final parameter that would indicate a ResultSet
   // or database CURSOR type exists in the java.sql.Types class.
   //
   // Thus, you have to consult the database JDBC driver documentation
   // to see how an output CURSOR/ResultSet should be registered.
   //
   // In most cases, the type info parameter sent in to the
   // registerOutParameter method is a type native to the
   // driver. The result: Non-standard type behavior...
   //
   // *Messy*
   //
   aStatement.registerOutParameter(2, MyDriverClass.RESULTSET_TYPE);
   aStatement.registerOutParameter(1, Types.<errorStatusType>);

   // Call the procedure in the database
   aStatement.execute();

   // Handle the output
   //
   // Unfortunately, there is no method getXXX that returns a
   // java.sql.ResultSet object in the java.sql.CallableStatement
   // interface.
   //
   // Thus, you have to consult the database JDBC driver documentation
   // to see what method you would call to interpret the outgoing
   // database CURSOR as a java.sql.ResultSet.
   //
   // *Messy*
   //
   ResultSet valueReceived = ((<SpecificDriverClass>) aStatement).getResultSet(2);
   int errorStatus = aStatement.get<errorStatusType>(1);

   // Handle the status code of the call
   if(errorStatus == <completelyMad>)
   {
       // Bail out
       throw new SQLException("Database call broken with status: " + errorStatus);
   }

   // Handle the received ResultSet
   while(valueReceived.next())
   {
       // ...
   }
}
catch(Exception ex)
{
   // Log an error message
   System.err.println("Database communication error: " + ex);
}

Базовый код JDBC для Сценариев 3 и 4
Третий сценарий удобнее. Он полностью зависит от стандартных для JDBC вызовов методов. Тем не менее, Не многие релятивные базы данных умеют обрабатывать используемый здесь синтаксис ({? = call calculate_living_costs()}). Но у него есть преимущество перед сценариями 1 и 2, потому что Java-код платформенно-независим. Таким образом, из соображений переносимости третий сценарий (приведенный ниже) представляет собой пока что наилучший вариант. Сценарий 4, выполняемый по тому же принципу, что и сценарий 3, вынужден иногда регистрировать получаемый на выходе ResultSet в виде параметра. Сверьтесь с документацией на драйвер.

// Database connection supposedly
// already made.
Connection conn;
CallableStatement aStatement = null;

try
{
   // Create the database callable statement.
   // For example, assume that the "<Some_SQL_Procedure_call>"
   // is something like
   //
   // Scenario 3: "{? = call calculate_living_costs()}"
   // where the '?' denotes an output parameter that should
   // be interpreted as a java.sql.ResultSet. If you use this syntax
   // you will need to register the output parameter, as per scenario
   // (1) and (2). For some drivers, you could use the same syntax as
   // scenario four below:
   //
   // Scenario 4: "{call calculate_living_costs()}"
   // The difference is essentially that the database procedure in
   // the fourth scenario hides any return parameters.
   // From a JDBC standpoint, however, the code is similar.
   aStatement = conn.prepareCall("<Some_SQL_Procedure_call>");

   // Thankfully, no need to register a ResultSet output parameter
   // as the return value of the stored procedure is a CURSOR/ResultSet.
   //
   // Most calls to procedures of this type would handle only single
   // ResultSets return values, though...
   //            
   // Call the procedure in the database
   ResultSet valueReceived = aStatement.executeQuery();

   // Handle the received ResultSet
   while(valueReceived.next())
   {
       // ...
   }
}
catch(Exception ex)
{
   // Log an error message
   System.err.println("Database communication error: " + ex);
}

Поддержка баз данных сценария для ResultSet CallableStatement
А как подошли к решению сценария ResultSet CallableStatement производители баз данных? Приведу примеры того, как реализовано возвращение одного ResultSet (сценарий для нескольких ResultSet будет описан в следующей статье):

Таблица 6. Базы данных и решения для сценария ResultSet CallableStatement, реализованные в них.

База данных
Решение сценария ResultSet CallableStatement
Oracle8i
// The scenario requires importing some oracle driver types
import oracle.jdbc.driver.*;
CallableStatement aStatement;
ResultSet valueReceived;
// Create a PL/SQL block to open the cursor
// This could be used to call a PL/SQL procedure as well.
aStatement = conn.prepareCall
  ("begin open ? for select * from gamePlayers; end;");
// Oracle uses the OracleTypes.CURSOR type integer to
// designate an output ResultSet
aStatement.registerOutParameter(1, OracleTypes.CURSOR);
// Execute the query
aStatement.execute();
// Oracle uses the non-standard method getCursor
// to receive the output ResultSet
valueReceived = ((OracleCallableStatement)aStatement).getCursor(1);
// Handle results
while (valueReceived.next())
{
       // ...
}[</code>]
Sybase Enterprise Server и
Microsoft SQL Server 7
// The two Transact-SQL databases use a similar technique
// to handle the ResultSet CallableStatement scenario:
//
// Only a single ResultSet may be returned in one call,
// unless lots of non-standard functionality should be used.
CallableStatement aStatement;
ResultSet valueReceived;
// Call a stored procedure returning a ResultSet
aStatement = conn.prepareCall
   ("{call getDataOnAllDogs}");
//Execute the stored procedure and get a result set from it.
valueReceived = aStatement.executeQuery();
// Handle results
while (valueReceived.next())
{
       // ...
}
Informix
// The Informix way to handle the ResultSet
// CallableStatement scenario conforms to the way of
// Sybase and Microsoft above.
CallableStatement aStatement;
ResultSet valueReceived;
// Call a stored procedure returning a ResultSet
aStatement = conn.prepareCall
   ("{call someStoredProcedure()}");
//Execute the stored procedure and get a result set from it.
valueReceived = aStatement.executeQuery();
// Handle results
while (valueReceived.next())
{
       // ...
}

И хотя последние два примера в таблице выглядят довольно-таки просто с точки зрения разработки Java-приложения, обратите внимание на то, что ни один из них не имеет дела с возвратом нескольких ResultSets от одного процедурного вызова.

Заключение
Я описал два из трёх принципиальных способов обращения к базе данных для получения информации с помощью JDBC API: стандартный сценарий для ResultSet и стандартный сценарий для Callable Statement (третий, который здесь не рассматривался, это иерархия классов в пакете java.sql). И все же, для достижения более мощной производительности, большего результата можно добиться, скомбинировав эти два сценария. Разработчики, которым приходится использовать сценарий ResultSet CallableStatement часто, могут, в принципе, создать прикладной API базы данных, который будет вызываться приложением Java-сервера, таким образом инкапсулировать физическую структуру базы данных.

Ценой за применение такого комбинированного сценария будет недостаток точных стандартов обращения, т.е. кое в чём вы потеряете присущую JDBC независимость от базы данных.


Об авторе
Lennart Jorelid является специалистом в области работы с серверными данными для электронной коммерции в компании jGuru Europe. Имея большой опыт работы на проектами в США, Канаде, Соединённом Королевстве, Швейцарии, Швеции и Германии, Леннарт является признанным экспертом, архитектором и пропагандистом технологии Java. В настоящее время он пишет книгу, посвящённую моделям применения Java на серверах. Увлекается лыжами, театром и научной фантастикой.

Дополнительные ресурсы (на английском языке)

<< НАЗАД