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

Пакетное изменение данных

Поскольку Firebird 4.0 поддерживает пакетное выполнение операторов с входными параметрами, это означаетотправку более чем одного набора параметров при выполнении оператора. Пакетный интерфейс разработан (в первую очередь)для удовлетворения требований JDBC по пакетной обработке подготовленных операторов, но имеет ряд серьезных отличий:

  • как и все операции с данными в firebird ориентированы на сообщения, а не на одно поле;

  • в качестве важного расширения пакетный интерфейс поддерживает встроенное использование BLOB-объектов(особенно эффективно при работе с небольшими BLOB-объектами);

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

Интерфейс IBatch (точно так же, как и IResultSet)может быть создан двумя способами – с использованием интерфейса IStatement илиIAttachment, в обоих случаях вызывается метод createBatch() соответствующего интерфейса.Во втором случае текст оператора SQL, который должен выполняться в пакете, передается непосредственно в createBatch().Настройка пакетной обработки осуществляется с помощью блока Batch parameters, формат которого более или менее похожна DPB v.2 – в начале тег (IBatch::CURRENT_VERSION), за которым следует набор широких скоплений: тег 1 байт, длина 4 байта,значение указанной длины байт. Возможные теги описаны в пакетном интерфейсе.Самый простой (и рекомендуемый) способ создать блок параметров для пакетного создания — использоватьсоответствующий интерфейс IXpbBuilder:

IXpbBuilder* pb = utl->getXpbBuilder(&status, IXpbBuilder::BATCH, NULL, 0);
pb->insertInt(&status, IBatch::RECORD_COUNTS, 1);

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

Чтобы создать пакетный интерфейс с нужными параметрами, передайте блок параметров в вызов createBatch():

IBatch* batch = att->createBatch(&status, tra, 0, sqlStmtText, SQL_DIALECT_V6, NULL,
  pb->getBufferLength(&status), pb->getBuffer(&status));

В этом примере пакетный интерфейс создается с форматом сообщений по умолчанию,потому что вместо формата входных метаданных передается NULL.

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

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

Конечно, если вы передали свой собственный формат сообщений в пакет, вы можете просто использовать его.

Далее я предполагаю, что существует некоторая функция fillNextMessage(unsigned char* data, IMessageMetadata* metadata)и она может заполнить буфер data в соответствии с переданным форматом metadata. Для работы с сообщениями нам нужен буфер для данных:

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

Теперь мы можем добавить в пакет несколько сообщений с заполненными данными:

fillNextMessage(data, meta);
batch->add(&status, 1, data);

fillNextMessage(data, meta);
batch->add(&status, 1, data);

Альтернативный способ работы с сообщениями (с помощью макроса FB_MESSAGE) присутствует в примере использованияпакетного интерфейса 11.batch.cpp.

Наконец, пакет должен быть выполнен:

IBatchCompletionState* cs = batch->execute(&status, tra);

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

unsigned total = cs->getSize(&status);

Теперь выводим состояние каждого сообщения:

for (unsigned p = 0; p < total; ++p) printf("Msg %u state %d\n", p, cs->getState(&status, p));

Когда закончите анализ состояния завершения, не забудьте его удалить:

cs->dispose();

Полный пример печати содержимого IBatchCompletionState находитсяв функции print_cs() в примере 11.batch.cpp.

Если по какой-то причине вы хотите сделать пакетные буферы пустыми, не выполняя их(т.е. подготовиться к обработке новой порции сообщений), используйте метод cancel():

batch->cancel(&status);

Как и у остальных наших интерфейсов доступа к данным, у IBatch есть специальный метод для его закрытия:

batch->close(&status);

Вместо этого можно использовать стандартный вызов release(), если вас не волнуют ошибки:

batch->release();

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

Note
Замечание

JDBC не рекомендует использовать слишком большие пакеты, например, "Oracle рекомендует сохранять размеры пакетов в диапазоне от 50 до 100".Firebird поддерживает большие пакеты, но в любом случае должен ограничивать максимальный размер пакета — см. TAG_BUFFER_BYTES_SIZE.Если общий размер сообщений превышает этот предел, возвращается ошибка isc_batch_too_big.Обратите внимание, что из-за глубокой буферизации пакетных данных при их отправке по сети вы не получите эту ошибку сразу,а только при сбросе буферов от клиента к серверу. Это может происходить как в методах add(),так и в методах execute()execute() выполняет финальную сброс буфера. Если вы все еще хотите выполнить пакет с сообщениями,которые заполнили буфер, вы можете это сделать (когда функция execute() вернула ошибку, просто повторите ее).Фактическое количество обработанных сообщений будет возвращено в IBatchCompletionState.Оптимальный размер пакета должен быть найден для каждого конкретного случая, но скорее всего, если он более 1000,то вряд ли вы получите серьезный прирост производительности.

Можно добавить более одного сообщения за один вызов в пакет. При этом помните, что сообщения должны бытьправильно выровнены, чтобы эта функция работала правильно. Требуемое выравнивание и выровненный размер сообщениядолжны быть получены из интерфейса IMessageMetadata, например:

unsigned aligned = meta->getAlignedLength(&status);

Позже этот размер пригодится при выделении массива сообщений и работе с ним:

unsigned char* data = new unsigned char[aligned * N]; // N is desired number of messages

for (int n = 0; n < N; ++n)
  fillNextMessage(&data[aligned * n], meta);

batch->add(&status, N, data);

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

BLOBs в целом несовместимы с пакетами — пакет эффективен, когда нужно передать на сервер много мелких данных за один шаг,BLOBs рассматриваются как большие объекты, и поэтому в целом нет смысла использовать их в пакетах.Но на практике часто случается, что BLOB не слишком велики — и в этом случае использование традиционного BLOB API(создание BLOB, передача сегментов на сервер, закрытие BLOB, передача идентификатора BLOB в сообщении) убивает производительность,особенно при использовании через WAN. Поэтому в firebird пакет поддерживает передачу BLOB на сервер вместе с другими сообщениями.Чтобы использовать эту возможность, в первую очередь должна быть установлена политика использования BLOB-объектов длясоздаваемого пакета (как опция в блоке параметров):

pb->insertInt(&status, IBatch::BLOB_IDS, IBatch::BLOB_IDS_ENGINE);

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

FB_MESSAGE(Msg, ThrowStatusWrapper,
  (FB_VARCHAR(5), id)
  (FB_VARCHAR(10), name)
  (FB_BLOB, desc)
) project(&status, master);

В этом случае, чтобы отправить сообщение, содержащее blob, на сервер, можно сделать что-то вроде этого:

project->id = ++idCounter;
project->name.set(currentName);

batch->addBlob(&status, descriptionSize, descriptionText, &project->desc);

batch->add(&status, 1, project.getData());

Если какой-то BLOB оказался достаточно большим, чтобы не поместиться в ваш существующий буфер,вы можете вместо перераспределения буфера использовать метод appendBlobData().Он добавляет больше данных к последнему добавленному BLOB.

batch->addBlob(&status, descriptionSize, descriptionText, &project->desc, bpbLength, bpb);

После добавления первой части BLOB получите следующую часть данных в descriptionText, с размером descriptionSize после чего:

batch->appendBlobData(&status, descriptionSize, descriptionText);

Это можно сделать в цикле, но будьте осторожны, чтобы не переполнить внутренние пакетные буферы — их размерконтролируется параметром BUFFER_BYTES_SIZE при создании интерфейса IBatch, но не может превышать 256 МБ(по умолчанию 16 МБ). Если вам нужно обработать такой большой BLOB (например, на фоне множества мелких — этим можнообъяснить использование пакетной обработки), просто используйте стандартный API IBlob и метод registerBlob интерфейса IBatch.

Еще один возможный выбор политики BLOB — BLOB_IDS_USER. Использование на первый взгляд не сильно меняется —перед вызовом addBlob() правильный и уникальный для каждого пакета идентификатор должен быть помещен в память,на которую ссылается последний параметр. Конечно, тот же идентификатор должен быть передан в сообщении для BLOB.Принимая во внимание, что генерация BLOB-идентификаторов движком происходит очень быстро, такая политика может показаться бесполезной,но представьте случай, когда вы получаете BLOB и другие данные в относительно независимых потоках (например, в блоках файла),а некоторые хорошие идентификаторы уже присутствуют в них. В таком случае использование предоставленных пользователемидентификаторов BLOB может значительно упростить код.

Note
Обратите внимание

В отличие от BLOB-объектов, созданных с помощью обычного createBlob(), BLOB-объекты, созданные с помощью интерфейса IBatch,по умолчанию являются потоковыми, а не сегментированными. Сегментированные BLOB не представляют ничего интересногопо сравнению с потоковым и поэтому не рекомендуются для использования в новых разработках. Мы поддерживаем этот форматтолько по соображениям обратной совместимости. Если вам действительно нужны сегментированные BLOB, то это значениепо умолчанию можно переопределить, вызвав:

batch->setDefaultBpb(&status, bpbLength, bpb);

Разумеется, переданный BPB может содержать и любые другие параметры создания BLOB. Как вы, возможно, уже заметили,вы также можете передать BPB напрямую в addBlob(), но если большинство BLOB-объектов, которые вы собираетесь добавить,имеют одинаковый формат, отличный от формата по умолчанию, то использование setDefaultBpb() немного более эффективно.Возвращаясь к сегментированным BLOB — вызов addBlob() добавит первый сегмент в BLOB, последующие вызовы appendBlobData()добавят дополнительные сегменты. Не забывайте, что размер сегмента ограничен 64Кб – 1, попытка передать больше данныхза один вызов вызовет ошибку.

Следующий шаг - работа с существующими потоками BLOB для чего используется метод addBlobStream().Используя его, можно добавить более одного BLOB в пакет за один вызов. Поток BLOB — это последовательность BLOB,каждый из которых начинается с заголовка BLOB. Заголовок должен быть правильно выровнен — для этого в интерфейсе IBatchпредусмотрен специальный вызов:

unsigned alignment = batch->getBlobAlignment(&status);

Предполагается, что все компоненты потока BLOB в пакете должны быть выровнены как минимум по границе выравнивания,включая размер порций потока, передаваемых в addBlobStream(), который должен быть кратен этому выравниванию.Заголовок содержит 3 поля: 8-байтовый идентификатор BLOB (должен быть ненулевым), 4-байтовый общий размер BLOB и 4-байтовый размер BPB.Общий размер BLOB-объекта включает в себя BPB внутри, т. е. всегда можно найти следующий BLOB-объект в потоке в байтразмера BLOB-объекта после заголовка (с учетом выравнивания). BPB (если присутствует, т.е. если размер BPB не равен нулю)помещается сразу после заголовка. После передачи данных BLOB-объекта BPB их формат зависит от типа BLOB-объекта —потокового или сегментированного. В случае потокового BLOB это простая последовательность байтов, имеющая размер blob-size – BPB-size.С сегментированным BLOB-объектом все немного сложнее: данные BLOB-объекта представляют собой набор сегментов,где каждый сегмент имеет следующий формат: размер сегмента 2 байта (это должно быть выровнено по границе IBatch::BLOB_SEGHDR_ALIGN),за которым следуют хранящиеся в нем 2 байта количества байт.

Когда в поток добавляется BLOB, его размер не всегда известен заранее. Чтобы не было слишком большого буферадля этого BLOB (помните, что размер должен быть указан в заголовке BLOB перед данными BLOB), можно использовать записьпродолжения BLOB. В заголовке BLOB вы оставляете размер BLOB со значением, известным при создании этого заголовка,и добавляете запись продолжения, которая имеет формат, абсолютно такой же, как и заголовок BLOB, но здесь идентификаторBLOB должен быть равен нулю, а размер BPB всегда также должен быть равен нулю. Обычно вам потребуется иметь одну записьпродолжения для каждого вызова addBlobStream().

Последний метод, используемый для работы с BLOB, стоит особняком от первых трех, которые передают данные BLOB вместес остальными пакетными данными — он необходим для регистрации в идентификаторе пакета BLOB, созданного с использованием стандартного BLOB API.Это может быть неизбежным, если нужно передать в пакет действительно большой BLOB. Не используйте идентификатор такогоBLOB в пакете напрямую — это приведет к ошибке недопустимого идентификатора BLOB во время пакетного выполнения. Вместо этого выполните:

batch->registerBlob(&status, &realId, &msg->desc);

Если политика BLOB заставляет механизм Firebird генерировать идентификаторы BLOB, этого кода достаточно,чтобы правильно зарегистрировать существующий BLOB в пакете. В других случаях вам нужно будет назначить правильный (из пакета POV) идентификатордля msg→desc.

Почти все упомянутые методы используются в 11.batch.cpp — используйте его, чтобы увидеть живой пример пакетной обработки в firebird.

Пару слов о доступе к пакетам из ISC API - можно выполнить подготовленный оператор ISC в пакетном режиме.Для поддержки этого добавлено две новые функций API, а именно fb_get_transaction_interface иfb_get_statement_interface, которые позволяют получить доступ к соответствующим интерфейсам,идентичным существующим дескрипторам ISC. Пример этого присутствует в 12.batch_isc.cpp.

Работа с событиями

Интерфейс событий не был завершен в Firebird 4.0, мы ожидаем, что вследующей версии будет что-то более интересное. Минимальная существующаяподдержка выглядит следующим образом:IAttachment содержит методqueEvents(), который выполняет почти те же функции, что и вызовisc_que_events(). Вместо пары параметров FPTR_EVENT_CALLBACK ast иvoid* arg, необходимых для вызова кода пользователя, когда в Firebirdпроисходит событие, используется интерфейс обратного вызоваIEventCallback. Это традиционный подход, который помогает избежатьнебезопасных бросков из void* в пользовательской функции. Другое важноеразличие заключается в том, что вместо идентификатора события (видаобработчика) эта функция возвращает ссылку на интерфейсIEvents, имеющий метод cancel(),используемый для остановки ожидании события. В отличие отидентификатора, который уничтожается автоматически при поступлениисобытия, интерфейс не может быть уничтожен автоматически, если событиеполучено непосредственно перед вызовом метода cancel(), то это вызоветsegfault из-за того, что интерфейс уже будет уничтожен. Поэтому послеполучения события интерфейс IEventsдолжен быть явно освобождён. Это может быть сделано, например, прямоперед запросом события из очереди в следующий раз:

events->release();
events = NULL;

events = attachment->queEvents(&status, this, eveLen, eveBuffer);

Установка указателя интерфейса в NULL полезна в случае возникновенияисключения в queEvents. В других аспектах обработка событий неизменилась по сравнению с ISC API. Для получения дополнительнойинформации используйте наш пример 08.events.cpp.