Пакетное изменение данных
Поскольку 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 поддерживает большие пакеты, но в любом случае должен ограничивать максимальный размер пакета — см. |
Можно добавить более одного сообщения за один вызов в пакет. При этом помните, что сообщения должны бытьправильно выровнены, чтобы эта функция работала правильно. Требуемое выравнивание и выровненный размер сообщениядолжны быть получены из интерфейса 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-объектов, созданных с помощью обычного
|
Разумеется, переданный 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
.