35.4. Асинхронная обработка команд

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

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

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

  • PQexec может возвратить только одну структуру PGresult. Если отправленная серверу строка-команда содержит множественные SQL-команды, то все структуры PGresult, кроме последней, отбрасываются функцией PQexec.

  • PQexec всегда собирает все результаты выполнения команды, буферизуя их в единственной структуре PGresult. В то время как для приложения это упрощает логику обработки ошибок, это может быть непрактично, когда результат содержит много строк.

Приложения, которым эти ограничения не подходят, могут вместо PQexec использовать базовые функции, на основе которых и построена функция PQexec: PQsendQuery и PQgetResult. Есть также функции PQsendQueryParams, PQsendPrepare, PQsendQueryPrepared, PQsendDescribePrepared и PQsendDescribePortal, которые можно использовать совместно с PQgetResult, чтобы продублировать функциональность PQexecParams, PQprepare, PQexecPrepared, PQdescribePrepared и PQdescribePortal соответственно.

PQsendQuery

Отправляет команду серверу, не ожидая получения результата. Если команда была отправлена успешно, то функция возвратит значение 1, в противном случае она возвратит 0 (тогда нужно воспользоваться функцией PQerrorMessage для получения дополнительной информации о сбое).

int PQsendQuery(PGconn *conn, const char *command);

После успешного вызова PQsendQuery вызовите PQgetResult один или несколько раз, чтобы получить результат. Функцию PQsendQuery нельзя вызвать повторно (на том же самом соединении) до тех пор, пока PQgetResult не вернёт нулевой указатель, означающий, что выполнение команды завершено.

PQsendQueryParams

Отправляет серверу команду и обособленные параметры, не ожидая получения результатов.

int PQsendQueryParams(PGconn *conn,
                      const char *command,
                      int nParams,
                      const Oid *paramTypes,
                      const char * const *paramValues,
                      const int *paramLengths,
                      const int *paramFormats,
                      int resultFormat);

Эта функция эквивалентна функции PQsendQuery, за исключением того, что параметры запроса можно указать отдельно от самой строки запроса. Эта функция обрабатывает свои параметры точно так же, как и функция PQexecParams. Аналогично функции PQexecParams, данная функция не будет работать при подключениях по протоколу версии 2.0, также она позволяет включить только одну команду в строку запроса.

PQsendPrepare

Посылает запрос на создание подготовленного оператора с данными параметрами и не дожидается завершения его выполнения.

int PQsendPrepare(PGconn *conn,
                  const char *stmtName,
                  const char *query,
                  int nParams,
                  const Oid *paramTypes);

Это асинхронная версия функции PQprepare. Она возвращает 1, если ей удалось отправить запрос, и 0 в противном случае. После её успешного вызова следует вызвать функцию PQgetResult, чтобы определить, успешно ли создал сервер подготовленный оператор. Эта функция обрабатывает свои параметры точно так же, как и функция PQprepare. Аналогично функции PQprepare, данная функция не будет работать при подключениях по протоколу версии 2.0.

PQsendQueryPrepared

Посылает запрос на выполнение подготовленного оператора с данными параметрами, не ожидая получения результата.

int PQsendQueryPrepared(PGconn *conn,
                        const char *stmtName,
                        int nParams,
                        const char * const *paramValues,
                        const int *paramLengths,
                        const int *paramFormats,
                        int resultFormat);

Эта функция подобна функции PQsendQueryParams, но команда, которая должна быть выполнена, задаётся путём указания предварительно подготовленного оператора, вместо задания строки запроса. Эта функция обрабатывает свои параметры точно так же, как и функция PQexecPrepared. Аналогично функции PQexecPrepared, данная функция не будет работать при подключениях по протоколу версии 2.0.

PQsendDescribePrepared

Отправляет запрос на получение информации об указанном подготовленном операторе и не дожидается завершения выполнения запроса.

int PQsendDescribePrepared(PGconn *conn, const char *stmtName);

Это асинхронная версия функции PQdescribePrepared. Она возвращает 1, если ей удалось отправить запрос, и 0 в противном случае. После её успешного вызова следует вызвать функцию PQgetResult для получения результата. Эта функция обрабатывает свои параметры точно так же, как и функция PQdescribePrepared. Аналогично функции PQdescribePrepared, данная функция не будет работать при подключениях по протоколу версии 2.0.

PQsendDescribePortal

Отправляет запрос на получение информации об указанном портале и не дожидается завершения выполнения запроса.

int PQsendDescribePortal(PGconn *conn, const char *portalName);

Это асинхронная версия функции PQdescribePortal. Она возвращает 1, если ей удалось отправить запрос, и 0 в противном случае. После её успешного вызова следует вызвать функцию PQgetResult для получения результата. Эта функция обрабатывает свои параметры точно так же, как и функция PQdescribePortal. Аналогично функции PQdescribePortal, данная функция не будет работать при подключениях по протоколу версии 2.0.

PQgetResult

Ожидает получения следующего результата после предшествующего вызова PQsendQuery, PQsendQueryParams, PQsendPrepare, PQsendQueryPrepared, PQsendDescribePrepared или PQsendDescribePortal и возвращает его. Когда команда завершена и результатов больше не будет, тогда возвращается нулевой указатель.

PGresult *PQgetResult(PGconn *conn);

Функция PQgetResult должна вызываться повторно до тех пор, пока она не вернёт нулевой указатель, означающий, что команда завершена. (Если она вызвана, когда нет ни одной активной команды, тогда PQgetResult просто возвратит нулевой указатель сразу же.) Каждый ненулевой результат, полученный от PQgetResult, должен обрабатываться с помощью тех же самых функций доступа к структуре PGresult, которые были описаны выше. Не забывайте освобождать память, занимаемую каждым результирующим объектом, с помощью функции PQclear, когда работа с этим объектом закончена. Обратите внимание, что PQgetResult заблокируется, только если какая-либо команда активна, а необходимые ответные данные ещё не были прочитаны функцией PQconsumeInput.

Примечание

Даже когда PQresultStatus показывает критическую ошибку, всё равно следует вызывать функцию PQgetResult до тех пор, пока она не возвратит нулевой указатель, чтобы позволить libpq полностью обработать информацию об ошибке.

Использование PQsendQuery и PQgetResult решает одну из проблем PQexec: если строка команды содержит несколько SQL-команд, то результаты каждой из них можно получить индивидуально. (Между прочим, это позволяет организовать частичное совмещение обработки: клиент может обрабатывать результаты одной команды, в то время как сервер ещё работает с более поздними запросами, содержащимися в той же самой строке-команде.)

Ещё одной часто требующейся функциональной возможностью, которую можно получить с помощью PQsendQuery и PQgetResult, является извлечение больших выборок по одной строке за раз. Это обсуждается в Разделе 35.5.

Сам по себе вызов PQgetResult всё же заставит клиента заблокироваться до те пор, пока сервер не завершит выполнение следующей SQL-команды. Этого можно избежать с помощью надлежащего использования ещё двух функций:

PQconsumeInput

Если сервер готов передать данные, принять их.

int PQconsumeInput(PGconn *conn);

PQconsumeInput обычно возвращает 1, показывая, что «ошибки нет», но возвращает 0, если имела место какая-либо проблема (в таком случае можно обратиться к функции PQerrorMessage за уточнением). Обратите внимание, что результат не говорит, были ли какие-либо входные данные фактически собраны. После вызова функции PQconsumeInput приложение может проверить PQisBusy и/или PQnotifies, чтобы посмотреть, не изменилось ли их состояние.

PQconsumeInput можно вызвать, даже если приложение ещё не готово иметь дело с результатом или уведомлением. Функция прочитает доступные данные и сохранит их в буфере, при этом обрабатывая условие готовности к чтению функции select(). Таким образом, приложение может использовать PQconsumeInput, чтобы немедленно обработать это состояние select(), а изучать результаты позже.

PQisBusy

Возвращает 1, если команда занята работой, то есть функция PQgetResult была бы заблокирована в ожидании ввода. Возвращаемое значение 0 показывает, что функция PQgetResult при её вызове гарантированно не будет заблокирована.

int PQisBusy(PGconn *conn);

Функция PQisBusy сама не будет пытаться прочитать данные с сервера; следовательно, сначала нужно вызвать PQconsumeInput, иначе состояние занятости никогда не прекратится.

В типичном приложении, использующем эти функции, будет главный цикл, в котором функции select() или poll() служат для организации ожидания наступления всех условий, на которые цикл должен отвечать. Одним из условий будет наличие ввода, доступного от сервера, что в терминах функции select() означает наличие данных, готовых для чтения на файловом дескрипторе, идентифицируемом с помощью PQsocket. Когда главный цикл обнаруживает ввод, готовый для чтения, он должен вызвать PQconsumeInput, чтобы прочитать этот ввод. Затем он может вызвать PQisBusy, а после неё уже PQgetResult, если PQisBusy возвратит false (0). Главный цикл может также вызвать PQnotifies, чтобы обнаружить сообщения NOTIFY (см. Раздел 35.8).

Клиент, который использует PQsendQuery/PQgetResult, может также попытаться отменить команду, которая всё ещё обрабатывается сервером; см. Раздел 35.6. Но, независимо от возвращаемого значения функции PQcancel, приложение должно продолжать обычную последовательность операций чтения результатов запроса, используя PQgetResult. Успешная отмена просто заставит команду завершиться раньше, чем она сделала бы в противном случае.

Используя функции, описанные выше, можно избежать блокирования, ожидая ввода от сервера баз данных. Однако всё же возможно, что приложение будет заблокировано в ожидании отправки вывода на сервер. Это бывает относительно нечасто, но может иметь место, если отправлены очень длинные SQL-команды или значения данных. (Однако это значительно более вероятно, если приложение отправляет данные через команду COPY IN.) Чтобы предотвратить эту возможность и достичь совершенно неблокирующего режима работы с базой данных, можно использовать следующие дополнительные функции.

PQsetnonblocking

Устанавливает неблокирующий статус подключения.

int PQsetnonblocking(PGconn *conn, int arg);

Устанавливает состояние подключения как неблокирующее, если arg равен 1, или блокирующее, если arg равен 0. Возвращает 0 в случае успешного завершения и -1 в случае ошибки.

В неблокирующем состоянии успешные вызовы функций PQsendQuery, PQputline, PQputnbytes, PQputCopyData и PQendcopy не блокируются; изменения сохраняются в локальном буфере вывода до тех пор, пока не будут сброшены. Неудачные вызовы возвращают ошибку, и их придётся повторить.

Обратите внимание, что функция PQexec не соблюдает неблокирующий режим. Если она вызывается, она всё равно работает в блокирующем режиме.

PQisnonblocking

Возвращает режим блокирования для подключения базы данных.

int PQisnonblocking(const PGconn *conn);

Возвращает 1, если подключение установлено в неблокирующем режиме, и 0, если режим блокирующий.

PQflush

Пытается сбросить любые выходные данные, стоящие в очереди, на сервер. Возвращает 0 в случае успеха (или если очередь на отправку пуста), -1 в случае сбоя по какой-либо причине или 1, если она ещё не смогла отправить все данные, находящиеся в очереди (этот случай может иметь место, только если соединение неблокирующее).

int PQflush(PGconn *conn);

После отправки любой команды или данных через неблокирующее подключение следует вызвать функцию PQflush. Если она возвратит 1, подождите, пока сокет станет готовым к чтению или записи. Если он станет готовым к записи, то вызовите PQflush снова. Если он станет готовым к чтению, то вызовите PQconsumeInput, а затем вызовите PQflush снова. Повторяйте до тех пор, пока PQflush не возвратит 0. (Необходимо выполнять проверку на состояние готовности к чтению и забирать входные данные с помощью PQconsumeInput, потому что сервер может заблокироваться, пытаясь отправить нам данные, например, сообщения NOTICE, и не будет читать наши данные до тех пор, пока мы не прочитаем его.) Как только PQflush возвратит 0, подождите, пока сокет не станет готовым к чтению, а затем прочитайте ответ, как описано выше.