FirebirdSQL logo
 Интерфейсы FirebirdНаписание плагинов 

Выполнение SQL операторов с входными параметрами

Существует два способа выполнения оператора с входными параметрами.Выбор правильного метода зависит от того, нужно ли вам выполнять егоболее одного раза, и знаете ли вы заранее формат параметров. Когда этотформат известен, и оператор нужно запускать только один раз, тогда выможете воспользоваться одиночным вызовом IAttachment::execute(). Впротивном случае сначала необходимо подготовить SQL-запрос, после чегоего можно выполнять многократно с различными параметрами.

Чтобы подготовить SQL оператор для выполнения, используйте методprepare() интерфейса IAttachment:

IStatement* stmt = att->prepare(&status, tra, 0,
    "UPDATE department SET budget = ? * budget + budget WHERE dept_no = ?",
    SQL_DIALECT_V6, IStatement::PREPARE_PREFETCH_METADATA);

Если вы не собираетесь использовать описание параметров из Firebird(т.е. вы можете предоставить эту информацию самостоятельно), используйтеIStatement::PREPARE_PREFETCH_NONE вместоIStatement::PREPARE_PREFETCH_METADATA — это немного снизитклиент/серверный трафик и сохранит ресурсы.

В ISC API структура XSQLDA используется для описания формата параметровоператора. Новый API не использует XSQLDA — вместо неё используетсяинтерфейс IMessageMetadata.Набор входных параметров (а также запись, взятая из курсора) описываетсяв Firebird API таким же образом, далее называемый сообщением.IMessageMetadata передаётся в качестве параметра в методы обменасообщениями между программой и движком базы данных. Существует многоспособов получить экземпляр IMessageMetadata, вот некоторые из них:

  • получить из IStatement;

  • построить используяIMetadataBuilder интерфейс;

  • иметь собственную реализацию этого интерфейса.

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

IMessageMetadata* meta = stmt->getInputMetadata(&status);

Или мы можем построить сообщение метаданных самостоятельно. Для этогопрежде всего нам необходимо получить интерфейс построителя:

IMetadataBuilder* builder = master->getMetadataBuilder(&status, 2);

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

Теперь необходимо задать индивидуальные характеристики полей впостроителе. Минимально необходимыми являются типы полей и длина длястроковых полей:

builder->setType(&status, 0, SQL_DOUBLE + 1);

builder->setType(&status, 1, SQL_TEXT + 1);
builder->setLength(&status, 1, 3);

Новый API использует старые константы для типов SQL, наименьший бит, каки раньше, используется для обозначения возможности принимать nullзначение. В некоторых случаях имеет смысл установить подтип (для BLOB),набор символов (для текстовых полей) или масштаб (для числовых полей).Наконец, пришло время получить экземпляр IMessageMetadata:

IMessageMetadata* meta = builder->getMetadata(&status);

Здесь мы не обсуждаем собственную реализацию IMessageMetadata. Если вамэто интересно, то вы можете посмотреть пример 05.user_metadata.cpp.

Итак, мы получили экземпляр описания метаданных входных параметров. Нодля работы с сообщением нам также необходим буфер. Размер буфераявляется одной из основных характеристик сообщений метаданных ивозвращается методом getMessageLength() из IMessageMetadata:

char* buffer = new char[meta->getMessageLength(&status)];

Чтобы иметь дело с отдельными значениями внутри буфера, смещение к нимдолжно быть принято в расчёт. IMessageMetadata знает о смещениях длявсех значений в сообщении, используя его, мы можем создавать указателина них:

double* percent_inc = (double*) &buffer[meta->getOffset(&status, 0)];
char* dept_no = &buffer[meta->getOffset(&status, 1)];

Кроме того, не забывайте установить NULL флаги:

short* flag = (short*)&buffer[meta->getNullOffset(&status, 0)];
*flag = 0;

flag = (short*) &buffer[meta->getNullOffset(&status, 1)];
*flag = 0;

После завершения манипуляций со смещениями, мы готовы получить значенияпараметров:

getInputValues(dept_no, percent_inc);

и выполнить подготовленный оператор:

stmt->execute(&status, tra, meta, buffer, NULL, NULL);

Два последних NULL в параметрах предназначены для выходных сообщений иобычно используются для оператора EXECUTE PROCEDURE.

Если вам не нужно получать метаданные из оператора и вы планируетевыполнить его только один раз, то вы можете выбрать более простой способ— используйте метод execute() из интерфейсаIAttachment:

att->execute(&status, tra, 0,
    "UPDATE department SET budget = ? * budget + budget WHERE dept_no = ?",
    SQL_DIALECT_V6, meta, buffer, NULL, NULL);

В этом случае вам вообще не нужно использоватьIStatement.

Пример того, как выполнить оператор UPDATE с параметрами, присутствует в02.update.cpp, вы также увидите, как возбужденное исключение втриггере/процедуре может быть перехвачено в программе на C++.

Открытие курсора и извлечение данных из него

Единственный способ получить строки данных, возвращаемых операторомSELECT в OO API — это использовать интерфейсIResultSet. Этот интерфейсвозвращается методом openCursor() как в IAttachment, так и в IStatement.openCursor() в большинстве аспектов похож на execute(), и решение какимобразом открыть курсор (с использованием подготовленного оператора илинепосредственно из интерфейса подключения) то же. В примерах03.select.cpp и 04.print_table.cpp используются оба способа.Обратите внимание на одно отличие метода openCursor() по сравнению сexecute() — никто не передает буфер для выходного сообщения вopenCursor(), он будет передан позже, когда данные будут извлечены изкурсора. Это позволяет открывать курсор с неизвестным форматом выходногосообщения (NULL передается вместо выходных метаданных). В этом случаеFirebird использует формат сообщения по умолчанию, который может бытьзапрошен через интерфейс IResultSet:

const char* sql = "select * from ..."; // some select statement

IResultSet* curs = att->openCursor(&status, tra, 0, sql, SQL_DIALECT_V6,
    NULL, NULL, NULL, NULL, 0);

IMessageMetadata* meta = curs->getMetadata(&status);

Позже эти метаданные могут использоваться для выделения буфера дляданных и разбора извлечённых строк.

В качестве альтернативы можно сначала подготовить оператор, получитьметаданные из подготовленного оператора и после этого открыть курсор.Это предпочтительный способ, если вы предполагаете, что курсор будетоткрыт более одного раза.

IStatement* stmt = att->prepare(&status, tra, 0, sql, SQL_DIALECT_V6,
    IStatement::PREPARE_PREFETCH_METADATA);

IMessageMetadata* meta = stmt->getOutputMetadata(&status);

IResultSet* curs = stmt->openCursor(&status, tra, NULL, NULL, NULL, 0);

Мы получили (тем или иным способом) экземпляр описания метаданныхвыходных полей (строк в наборе данных). Для работы с сообщением намтакже нужен буфер:

unsigned char* buffer = new unsigned char[meta->getMessageLength(&status)];

В IResultSet есть много различных методов выборки, но когда курсороткрыт не с параметром SCROLL, то работает только fetchNext(), то естьможно перемещаться по записям только вперед. В дополнение к ошибкам ипредупреждениям в статусе метод fetchNext() возвращает код завершения,который может иметь значения RESULT_OK (когда буфер заполняетсязначениями для следующей строки) или RESULT_NO_DATA (когда в курсоребольше строк не осталось). RESULT_NO_DATA не является состоянием ошибки,это нормальное состояние после завершения метода, которое сигнализирует,что данных в курсоре больше нет. Если используется оболочка статуса(Status Wrapper), то исключение не бросается в случае возврата ошибки.Может быть возвращено еще одно значение — RESULT_ERROR — оно означаетотсутствие данных в буфере и ошибки в статусе векторе. Метод fetchNext()обычно вызывается в цикле:

while (curs->fetchNext(&status, buffer) == IStatus::RESULT_OK)
{
    // row processing
}

То, что происходит при обработке строк, зависит от ваших потребностей.Для получения доступа к определённому полю следует использовать смещениеполя:

unsigned char* field_N_ptr = buffer + meta->getOffset(&status, n);

где n - номер поля в сообщении. Этот указатель должен быть присвоенсоответствующему типу, в зависимости от типа поля. Например, для поляVARCHAR, следует использовать приведение к структуре vary:

vary* v_ptr = (vary*) (buffer + meta->getOffset(&status, n));

Теперь мы можем напечатать значение поля:

printf("field %s value is %*.*s\n",
       meta->getField(&status, n),
       v_ptr->vary_length,
       v_ptr->vary_length,
       v_ptr->vary_string);

Если вам нужна максимальная производительность, будет полезно кэшироватьнеобходимые значения метаданных, как это сделано в наших примерах03.select.cpp и 04.print_table.cpp.

Использование макросов FB_MESSAGE для статических сообщений

Работа с данными с использованием смещений довольно эффективна, нотребует написания большого количества кода. В C++ эту проблему можнорешить с помощью шаблонов, но даже по сравнению с ними наиболее удобнымспособом работы с сообщением является представление его в родном (длязаданного языка) форме — структуре в C/C++, записи в Pascal и т. д.Конечно это работает только в том случае, если формат сообщения известензаранее. Для создания таких структур в C++ в Firebird существуетспециальный макрос FB_MESSAGE.

FB_MESSAGE имеет 3 аргумента: имя сообщения (структуры), тип обёрткистатуса (status wrapper) и список полей. Использование первого и второгоаргумента очевидно, список полей содержит пары (field_type, field_name),где field_type является одним из следующих:

  • FB_BIGINT

  • FB_BLOB

  • FB_BOOLEAN

  • FB_CHAR(len)

  • FB_DATE

  • FB_DECFLOAT16

  • FB_DECFLOAT34

  • FB_DOUBLE

  • FB_FLOAT

  • FB_INTEGER

  • FB_INTL_CHAR(len, charSet)

  • FB_INTL_VARCHAR(len, charSet)

  • FB_SCALED_BIGINT(x)

  • FB_SCALED_INTEGER(x)

  • FB_SCALED_SMALLINT(x)

  • FB_SMALLINT

  • FB_TIME

  • FB_TIME_TZ

  • FB_TIME_TZ_EX

  • FB_TIMESTAMP

  • FB_TIMESTAMP_TZ

  • FB_TIMESTAMP_TZ_EX

  • FB_VARCHAR(len)

В сгенерированной препроцессором структуре типы integer и floatсопоставляются с соответствующими типами C, типы date и time — склассами FbDate иFbTime (все упомянутые здесь классынаходятся в пространстве имен Firebird), тип timestamp — с классомFbTimestamp, содержащим два публичныхчлена данных дату и время соответствующих классов, тип char — соструктурой FbChar и varchar — со структуройFbVarChar. Для каждого поля препроцессорсоздаст два члена данных — name для значения поля/параметра и nameNullдля индикатора NULL. Конструктор сообщений имеет 2 параметра — указательна оболочку статуса (status wrapper) и главный интерфейс (masterinterface):

FB_MESSAGE(Output, ThrowStatusWrapper,
    (FB_SMALLINT, relationId)
    (FB_CHAR(31), relationName)
    (FB_VARCHAR(100), description)
) output(&status, master);

Для статических сообщений использование FB_MESSAGE является самым лучшимвыбором, в то же время они легко могут быть переданы в методы execute,openCursor и fetch:

rs = att->openCursor(&status, tra, 0, sqlText,
      SQL_DIALECT_V6, NULL, NULL, output.getMetadata(), NULL, 0);

и используется для работы со значениями отдельных полей:

while (rs->fetchNext(&status, output.getData()) == IStatus::RESULT_OK)
{
  printf("%4d %31.31s %*.*s\n", output->relationId, output->relationName.str,
    output->descriptionNull ? 0 : output->description.length,
    output->descriptionNull ? 0 : output->description.length,
    output->description.str);
}

Пример использования макроса FB_MESSAGE для работы с сообщениямиприведен в примере 06.fb_message.cpp.

Работа с BLOB

Для BLOBs Firebird хранит в буфере сообщения идентификатор BLOB — 8байтовый объект, который должен быть выравнен по 4-байтной границе.Идентификатор имеет тип ISC_QUAD. ИнтерфейсIAttachment имеет 2 метода дляработы с BLOB — openBlob() и createBlob(), возвращающие интерфейсIBlob и имеющие одинаковый наборпараметров, но выполняющие несколько разные действия: openBlob()принимает BLOB идентификатор из сообщения и подготавливает BLOB длячтения, а createBlob() создает новый BLOB, помещает его идентификатор всообщение и подготавливает BLOB для записи.

Для работы с BLOBs прежде всего необходимо включить в сообщение ихBLOB-идентификаторы. Если вы получите метаданные из поля движка Firebirdсоответствующего типа, то этот идентификатор уже будет присутствовать. Вэтом случае вы просто используете его смещение (при условии, чтопеременная blobFieldNumber содержит номер поля BLOB) (и соответствующееNULL смещение для проверки NULL или установки NULL флага) для полученияуказателя в буфере сообщений:

ISC_QUAD* blobPtr =
  (ISC_QUAD*) &buffer[metadata->getOffset(&status, blobFieldNumber)];
ISC_SHORT* blobNullPtr =
  (ISC_SHORT*) &buffer[metadata->getNullOffset(&status, blobFieldNumber)];

Если вы используете статические сообщениями макрос FB_MESSAGE, то полеBLOB будет объявлено как тип FB_BLOB:

FB_MESSAGE(Msg, ThrowStatusWrapper,
    (FB_BLOB, b)
) message(&status, master);

ISC_QUAD* blobPtr = &message->b;
ISC_SHORT* blobNullPtr = &message->bNull;

Для создания нового BLOB, вызовите метод createBlob():

IBlob* blob = att->createBlob(status, tra, blobPtr, 0, NULL);

Последние два параметра требуются только в том случае, если вы хотитеиспользовать blob-фильтры или blob-поток, которые не рассматриваютсяздесь.

Теперь Blob интерфейс готов принять данные в BLOB. Используйте методputSegment() для отправки данных в движок:

void* segmentData;
unsigned segmentLength;
while (userFunctionProvidingBlobData(&segmentData, &segmentLength))
    blob->putSegment(&status, segmentLength, segmentData);

После отправки некоторых данных в BLOB не забудьте закрытьblob-интерфейс:

blob->close(&status);

Убедитесь, что null флаг не установлен (не требуется, если вы сбросиливесь буфер сообщений перед созданием BLOB):

*blobNullPtr = 0;

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

Чтобы прочитать blob, необходимо получить его идентификатор в сообщенииот ядра firebird. Это можно сделать с помощью методов fetch() илиexecute(). После этого используйте метод openBlob():

IBlob* blob = att->openBlob(status, tra, blobPtr, 0, NULL);

Blob интерфейс готов предоставить данные BLOB. Используйте методgetSegment() для получения данных из движка:

char buffer[BUFSIZE];
unsigned actualLength;

for(;;)
{
  switch (blob->getSegment(&status, sizeof(buffer), buffer, &actualLength))
  {
    case IStatus::RESULT_OK:
      userFunctionAcceptingBlobData(buffer, actualLength, true);
      continue;

    case IStatus::RESULT_SEGMENT:
      userFunctionAcceptingBlobData(buffer, actualLength, false);
      continue;

    default:
      break;
  }
}

Последний параметр в userFunctionAcceptingBlobData() — это флагдостижения конца сегмента — когда getSegment() возвращает код завершенияRESULT_SEGMENT, о чём будет уведомлена функция (в последний параметрпередан false), то есть этот сегмент прочитан не полностью, ипродолжение ожидается при следующем вызове.

Закончив работать с BLOB, не забудьте закрыть его:

blob->close(&status);