Большие объекты

В QHB имеется механизм для работы с большими объектами, обеспечивающий потоковый доступ к пользовательским данным, хранящимся в специальной структуре больших объектов. Потоковый доступ полезен при работе со значениями данных, которые слишком велики, чтобы успешно обрабатывать их как единое целое.

В этой главе описываются реализация и интерфейсы языка программирования и запросов к данным больших объектов в QHB. Здесь для примеров используется библиотека libpq для языка C, но ту же функциональность поддерживает большинство программных интерфейсов QHB. Другие интерфейсы могут внутренне использовать интерфейс больших объектов для обеспечения универсального подхода к работе с большими значениями. В данной главе это не описывается.



Введение

Все большие объекты хранятся в одной системной таблице с именем pg_largeobject. Также для каждого большого объекта имеется запись в системной таблице pg_largeobject_metadata. Большие объекты можно создавать, изменять и удалять с помощью API чтения/записи, аналогичного стандартным операциям с файлами.

Кроме того, QHB поддерживает систему хранения под названием «TOAST», которая автоматически сохраняет значения в таблице, занимающие более одной страницы базы данных, во вторичную область хранения. Из-за этого механизм для работы с большими объектами оказывается отчасти устаревшим. Одно из оставшихся преимуществ больших объектов заключается в том, что они позволяют использовать значения размером до 4 ТБ, тогда как поля в TOAST могут иметь объем не более 1 ГБ. Кроме того, чтение и изменение большого объекта можно успешно выполнять по частям, тогда как при большинстве операций с полем в TOAST значение будет считываться или записываться как единое целое.



Особенности реализации

Программная реализация работы с большими объектами состоит в том, что те разбиваются на «фрагменты», которые сохраняются в строках таблиц в базе данных. При произвольном доступе на чтение и запись быстрый поиск правильного номера фрагмента обеспечивает индекс B-дерево.

Фрагменты сохраненного большого объекта необязательно должны быть смежными. Например, если приложение открывает новый большой объект, перемещается к смещению 1000000 байт и записывает там несколько байтов, это не приводит к выделению дополнительных 1000000 байт в хранилище; там будут размещены только фрагменты, охватывающие диапазон фактически записанных байтов данных. Однако операция чтения будет считывать нули для любых нераспределенных участков, предшествующих последнему существующему фрагменту. Это соответствует обычному поведению «разреженных» файлов в файловых системах Unix.

У больших объектов имеется владелец и набор прав доступа, которыми можно управлять с помощью команд GRANT и REVOKE. Для чтения большого объекта требуются права SELECT, а для записи или опустошения — права UPDATE. Удалять большой объект, добавлять для него комментарий или менять его владельца может только его владелец (или суперпользователь базы данных). Это поведение можно скорректировать, изменив параметр времени выполнения lo_compat_privileges.



Клиентские интерфейсы

В этом разделе описываются средства, которые библиотека клиентского интерфейса QHB libpq предоставляет для доступа к большим объектам. Интерфейс больших объектов QHB похож на интерфейс файловой системы Unix, с аналогами функций open, read, write, lseek и т. д.

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

Если при выполнении любой из этих функций возникает ошибка, то функция возвращает значение в ином случае невозможное, обычно 0 или -1. Сообщение, описывающее ошибку, хранится в объекте соединения и может быть извлечено с помощью функции PQerrorMessage.

Клиентские приложения, использующие эти функции, должны включать файл заголовка libpq/libpq-fs.h и иметь возможность соединяться с библиотекой libpq.

Клиентские приложения не могут использовать эти функции, пока соединение libpq находится в режиме конвейерной обработки.


Создание большого объекта

Функция

Oid lo_create(PGconn *conn, Oid Id_БО);

создает новый большой объект. Назначаемый OID можно задать с помощью аргумента Id_БО; если этот OID уже используется для некоторого большого объекта, возникает ошибка. Если Id_БО имеет значение InvalidOid (ноль), то lo_create назначает неиспользуемый OID. Возвращаемое значение — это OID, присвоенный новому большому объекту, или InvalidOid (ноль) при ошибке.

Пример:

inv_oid = lo_create(conn, желаемый_oid);

Импорт большого объекта

Чтобы импортировать в качестве большого объекта файл операционной системы, вызовите:

Oid lo_import(PGconn *conn, const char *имя_файла);

В аргументе имя_файла задается имя файла операционной системы, который нужно импортировать как большой объект. Возвращаемое значение — это OID, присвоенный новому большому объекту, или InvalidOid (ноль) при ошибке. Обратите внимание, что файл считывается библиотекой клиентского интерфейса, а не сервером, поэтому он должен существовать в файловой системе клиента и быть доступен для чтения клиентским приложением.

Функция

Oid lo_import_with_oid(PGconn *conn, const char *имя_файла, Oid Id_БО);

также импортирует новый большой объект. Назначаемый OID можно задать с помощью аргумента Id_БО; если этот OID уже используется для некоторого большого объекта, то возникает ошибка. Если Id_БО имеет значение InvalidOid (ноль), то lo_import_with_oid назначает неиспользуемый OID (это поведение аналогично lo_import). Возвращаемое значение — это OID, присвоенный новому большому объекту, или InvalidOid (ноль) при ошибке.


Экспорт большого объекта

Чтобы экспортировать большой объект в файл операционной системы, вызовите

int lo_export(PGconn *conn, Oid Id_БО, const char *имя_файла);

В аргументе Id_БО указывается OID большого объекта для экспорта, а в аргументе имя_файла — имя файла в операционной системе. Обратите внимание, что файл записывается библиотекой клиентского интерфейса, а не сервером. Возвращает 1 при успешном выполнении, -1 при ошибке.


Открытие существующего большого объекта

Чтобы открыть существующий большой объект для чтения или записи, вызовите

int lo_open(PGconn *conn, Oid Id_БО, int режим);

В аргументе Id_БО задается OID открываемого большого объекта. Биты в аргументе режим определяют, будет ли объект открыт для чтения (INV_READ), записи (INV_WRITE) или того и другого. (Эти символьные константы определены в заголовочном файле libpq/libpq-fs.х.) lo_open возвращает дескриптор большого объекта (неотрицательный) для последующего использования в lo_read, lo_write, lo_lseek, lo_lseek64, lo_tell, lo_tell64, lo_truncate, lo_truncate64 и lo_close. Дескриптор действителен только во время выполнения текущей транзакции. При ошибке возвращается значение -1.

В настоящее время сервер не различает режимы INV_WRITE и INV_READ | INV_WRITE: читать данные из дескриптора разрешено в любом случае. Однако между этими режимами и режимом INV_READ есть существенная разница: в режиме INV_READ записывать данные в дескриптор нельзя, а считанные из него данные будут отображать содержимое большого объекта во время снимка состояния транзакции, который был активен при выполнении lo_open, независимо от последующих записей, сделанных этой или другими транзакциями. Чтение из дескриптора, открытого с помощью INV_WRITE, возвращает данные, отражающие все операции записи других зафиксированных транзакций, а также операции записи текущей транзакции. Это похоже на отличия режимов REPEATABLE READ и READ COMMITTED для обычных команд SQL SELECT.

Функция lo_open завершится ошибкой, если у пользователя нет права SELECT для этого большого объекта или если указан режим INV_WRITE и отсутствует право UPDATE. Эти проверки наличия прав можно выключить с помощью параметра времени выполнения lo_compat_privileges.

Пример:

inv_fd = lo_open(conn, inv_oid, INV_READ|INV_WRITE);

Запись данных в большой объект

Функция

int lo_write(PGconn *conn, int fd, const char *буфер, size_t длина);

записывает длину байт из буфера (который должен иметь размер, равный длине) в дескриптор большого объекта fd. В аргументе fd должно стоять значение, возвращенное предыдущим вызовом lo_open. Возвращается количество фактически записанных байтов (в текущей реализации это значение всегда будет равно длине, если не возникает ошибка). В случае ошибки возвращаемое значение равно -1.

Хотя параметр длина объявляется как size_t, эта функция будет отклонять значения длины, превышающие INT_MAX. На практике все равно лучше всего передавать данные фрагментами не больше нескольких мегабайтов.


Чтение данных из большого объекта

Функция

int lo_read(PGconn *conn, int fd, char *буфер, size_t длина);

читает до длины байт из дескриптора большого объекта fd в буфер (который должен иметь размер, равный длине). В аргументе fd должно стоять значение, возвращенное предыдущим вызовом lo_open. Возвращается количество фактически прочитанных байтов; это число будет меньше длины, если при чтении был достигнут конец большого объекта. В случае ошибки возвращаемое значение равно -1.

Хотя параметр длина объявляется как size_t, эта функция будет отклонять значения длины, превышающие INT_MAX. На практике все равно лучше всего передавать данные фрагментами не больше нескольких мегабайтов.


Перемещение в большом объекте

Чтобы изменить текущее местоположение чтения или записи, связанное с дескриптором большого объекта, вызовите

int lo_lseek(PGconn *conn, int fd, int смещение, int whence);

Эта функция перемещает указатель текущего местоположения для дескриптора большого объекта, определенного с помощью fd, к новому местоположению, заданному аргументом смещение. Допустимыми значениями для аргумента whence являются SEEK_SET (перемещение от начала объекта), SEEK_CUR (перемещение с текущей позиции) и SEEK_END (перемещение от конца объекта). Возвращаемое значение — это указатель нового местоположения или -1 при ошибке.

При работе с большими объектами, размер которых может превышать 2 ГБ, вместо этого используйте

pg_int64 lo_lseek64(PGconn *conn, int fd, pg_int64 смещение, int whence);

Эта функция ведет себя так же, как и lo_lseek, но может принять значение смещение, превышающее 2 ГБ и/или вернуть результат, превышающий 2 ГБ. Обратите внимание, что если указатель нового местоположения больше 2 ГБ, то lo_lseek выдаст ошибку.


Получение текущего положения в большом объекте

Чтобы получить текущее местоположение чтения или записи для дескриптора большого объекта, вызовите

int lo_tell(PGconn *conn, int fd);

Если возникает ошибка, возвращаемое значение равно -1.

При работе с большими объектами, размер которых может превышать 2 ГБ, вместо этого используйте

pg_int64 lo_tell64(PGconn *conn, int fd);

Эта функция ведет себя так же, как и lo_tell, но может вернуть результат, превышающий 2 ГБ. Обратите внимание, что если текущее местоположение чтения/записи больше 2 ГБ, то lo_tell выдаст ошибку.


Усечение большого объекта

Чтобы усечь большой объект до заданной длины, вызовите

int lo_truncate(PGcon *conn, int fd, size_t длина);

Эта функция усекает дескриптор большого объекта fd до заданной длины. В аргументе fd должно стоять значение, возвращенное предыдущим вызовом lo_open. Если длина превышает текущую длину большого объекта, тот расширяется до заданной длины нулевыми байтами ('\0'). В случае успеха lo_truncate возвращает ноль. При ошибке возвращается значение -1.

Местоположение чтения/записи, связанное с дескриптором fd, не изменяется.

Хотя параметр длина объявлен как size_t, lo_truncate будет отклонять значения длины, превышающие INT_MAX.

При работе с большими объектами, размер которых может превышать 2 ГБ, вместо этого используйте

int lo_truncate64(PGcon *conn, int fd, pg_int64 длина);

Эта функция ведет себя так же, как и lo_truncate, но может принимать длину, превышающую 2 ГБ.


Закрытие дескриптора большого объекта

Дескриптор большого объекта можно закрыть, вызвав

int lo_close(PGconn *conn, int fd);

где fd — это дескриптор большого объекта, возвращенный функцией lo_open. В случае успеха lo_close возвращает ноль. При ошибке возвращается значение -1.

Все дескрипторы больших объектов, которые остаются открытыми в конце транзакции, будут закрыты автоматически.


Удаление большого объекта

Чтобы удалить большой объект из базы данных, вызовите

int lo_unlink(PGconn *conn, Oid Id_БО);

В аргументе Id_БО указывается OID большого объекта, подлежащего удалению. Возвращает 1 в случае успеха и -1 при ошибке.



Серверные функции

Серверные функции, предназначенные для работы с большими объектами из SQL, перечислены в Таблице 1.

Таблица 1. SQL-ориентированные функции для больших объектов

Функция
Описание
Пример(ы)
lo_from_bytea ( id_бо oid, данные bytea ) → oid
Создает большой объект и сохраняет в нем переданные данные. Если id_бо равен нулю, то система выберет свободный OID; в противном случае используется заданный OID (если этот OID уже присвоен какому-то большому объекту, возникает ошибка). В случае успеха возвращается OID созданного большого объекта.
lo_from_bytea(0, '\xffffff00') → 24528
lo_put ( id_бо oid, смещение bigint, данные bytea ) → void
Записывает данные в большой объект, начиная с заданного смещения; при необходимости большой объект расширяется.
lo_put(24528, 1, '\xaa') →
lo_get ( id_бо oid [, смещение bigint, длина integer ] ) → bytea
Извлекает содержимое большого объекта или его подстроку.
lo_get(24528, 0, 3) → \xffaaff

Каждой из описанных ранее клиентских функций соответствуют дополнительные серверные функции; на самом деле по большей части клиентские функции представляют собой просто интерфейсы к аналогичным серверным функциям. К функциям, которые так же удобно вызывать с помощью команд SQL, относятся lo_create, lo_unlink, lo_import и lo_export. Вот примеры их использования:

CREATE TABLE image (
    name            text,
    raster          oid
);

SELECT lo_create(43213);   -- пытается создать большой объект с OID 43213

SELECT lo_unlink(173454);  -- удаляет большой объект с OID 173454

INSERT INTO image (name, raster)
    VALUES ('beautiful image', lo_import('/etc/motd'));

INSERT INTO image (name, raster)  -- то же, что выше, но с заданным OID
    VALUES ('beautiful image', lo_import('/etc/motd', 68583));

SELECT lo_export(image.raster, '/tmp/motd') FROM image
    WHERE name = 'beautiful image';

Серверные функции lo_import и lo_export ведут себя совершенно иначе, нежели их аналоги на стороне клиента. Эти две функции читают и записывают файлы в файловой системе сервера, используя разрешения пользователя-владельца базы данных. Поэтому по умолчанию их могут использовать только суперпользователи. Функции же импорта и экспорта на стороне клиента, напротив, считывают и записывают файлы в файловой системе клиента, используя разрешения клиентской программы. Выполнение клиентских функций не требует никаких прав доступа к базе данных, за исключением права на чтение или запись соответствующего большого объекта.

ВНИМАНИЕ!
С помощью команды GRANT можно разрешить использование серверных функций lo_import и lo_export не только суперпользователям, но при этом необходимо внимательно рассмотреть последствия для безопасности. Злонамеренный пользователь, имеющий такие права, может с легкостью воспользоваться ими, чтобы стать суперпользователем (например, путем перезаписи файлов конфигурации сервера), или атаковать остальную файловую систему сервера, не утруждаясь получением собственно прав суперпользователя базы данных. Поэтому доступ к ролям, имеющим подобные права, следует контролировать так же тщательно, как и доступ к ролям суперпользователя. Тем не менее если для выполнения некоторых рутинных задач требуется применение серверных функций lo_import или lo_export, безопаснее использовать роль с такими правами, чем с полными правами суперпользователя, поскольку это помогает снизить риск сбоев от случайных ошибок.

Функциональные возможности lo_read и lo_write также предоставляются через вызовы на стороне сервера, но имена серверных функций, в отличие от имен интерфейсов на стороне клиента, не содержат подчеркиваний. Эти функции следует вызывать как loread и lowrite.



Пример программы

Данный пример — простая программа, которая показывает, как можно использовать интерфейс больших объектов в libpq. Части этой программы закомментированы, но оставлены в тексте для удобства читателя.

Большие объекты с примером программы libpq

#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include "libpq-fe.h"
#include "libpq/libpq-fs.h"

#define BUFSIZE         1024

/*
 * importFile -
 *    импортировать файл "in_filename" в базу данных как большой объект "lobjOid"
 *
 */
static Oid
importFile(PGconn *conn, char *filename)
{
    Oid         lobjId;
    int         lobj_fd;
    char        buf[BUFSIZE];
    int         nbytes,
                tmp;
    int         fd;

    /*
     * открыть файл для чтения
     */
    fd = open(filename, O_RDONLY, 0666);
    if (fd < 0)
    {                           /* ошибка */
        fprintf(stderr, "cannot open unix file\"%s\"\n", filename);
    }

    /*
     * создать большой объект
     */
    lobjId = lo_creat(conn, INV_READ | INV_WRITE);
    if (lobjId == 0)
        fprintf(stderr, "cannot create large object");

    lobj_fd = lo_open(conn, lobjId, INV_WRITE);

    /*
     * прочитать данные из файла Unix и записать их в инверсионный файл
     */
    while ((nbytes = read(fd, buf, BUFSIZE)) > 0)
    {
        tmp = lo_write(conn, lobj_fd, buf, nbytes);
        if (tmp < nbytes)
            fprintf(stderr, "error while reading \"%s\"", filename);
    }

    close(fd);
    lo_close(conn, lobj_fd);

    return lobjId;
}

static void
pickout(PGconn *conn, Oid lobjId, int start, int len)
{
    int         lobj_fd;
    char       *buf;
    int         nbytes;
    int         nread;

    lobj_fd = lo_open(conn, lobjId, INV_READ);
    if (lobj_fd < 0)
        fprintf(stderr, "cannot open large object %u", lobjId);

    lo_lseek(conn, lobj_fd, start, SEEK_SET);
    buf = malloc(len + 1);

    nread = 0;
    while (len - nread > 0)
    {
        nbytes = lo_read(conn, lobj_fd, buf, len - nread);
        buf[nbytes] = '\0';
        fprintf(stderr, ">>> %s", buf);
        nread += nbytes;
        if (nbytes <= 0)
            break;              /* больше данных не нужно? */
    }
    free(buf);
    fprintf(stderr, "\n");
    lo_close(conn, lobj_fd);
}

static void
overwrite(PGconn *conn, Oid lobjId, int start, int len)
{
    int         lobj_fd;
    char       *buf;
    int         nbytes;
    int         nwritten;
    int         i;

    lobj_fd = lo_open(conn, lobjId, INV_WRITE);
    if (lobj_fd < 0)
        fprintf(stderr, "cannot open large object %u", lobjId);

    lo_lseek(conn, lobj_fd, start, SEEK_SET);
    buf = malloc(len + 1);

    for (i = 0; i < len; i++)
        buf[i] = 'X';
    buf[i] = '\0';

    nwritten = 0;
    while (len - nwritten > 0)
    {
        nbytes = lo_write(conn, lobj_fd, buf + nwritten, len - nwritten);
        nwritten += nbytes;
        if (nbytes <= 0)
        {
            fprintf(stderr, "\nWRITE FAILED!\n");
            break;
        }
    }
    free(buf);
    fprintf(stderr, "\n");
    lo_close(conn, lobj_fd);
}


/*
 * exportFile -
 *    экспортировать большой объект "lobjOid" в файл "out_filename"
 *
 */
static void
exportFile(PGconn *conn, Oid lobjId, char *filename)
{
    int         lobj_fd;
    char        buf[BUFSIZE];
    int         nbytes,
                tmp;
    int         fd;

    /*
     * открыть большой объект
     */
    lobj_fd = lo_open(conn, lobjId, INV_READ);
    if (lobj_fd < 0)
        fprintf(stderr, "cannot open large object %u", lobjId);

    /*
     * открыть файл для записи
     */
    fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if (fd < 0)
    {                           /* ошибка */
        fprintf(stderr, "cannot open unix file\"%s\"",
                filename);
    }

    /*
     * прочитать данные из инверсионного файла и записать их в файл Unix
     */
    while ((nbytes = lo_read(conn, lobj_fd, buf, BUFSIZE)) > 0)
    {
        tmp = write(fd, buf, nbytes);
        if (tmp < nbytes)
        {
            fprintf(stderr, "error while writing \"%s\"",
                    filename);
        }
    }

    lo_close(conn, lobj_fd);
    close(fd);
}

static void
exit_nicely(PGconn *conn)
{
    PQfinish(conn);
    exit(1);
}

int
main(int argc, char **argv)
{
    char       *in_filename,
               *out_filename;
    char       *database;
    Oid         lobjOid;
    PGconn     *conn;
    PGresult   *res;

    if (argc != 4)
    {
        fprintf(stderr, "Usage: %s database_name in_filename out_filename\n",
                argv[0]);
        exit(1);
    }

    database = argv[1];
    in_filename = argv[2];
    out_filename = argv[3];

    /*
     * установить соединение
     */
    conn = PQsetdb(NULL, NULL, NULL, NULL, database);

    /* проверить, что соединение с сервером было успешно установлено */
    if (PQstatus(conn) != CONNECTION_OK)
    {
        fprintf(stderr, "%s", PQerrorMessage(conn));
        exit_nicely(conn);
    }

    /* Задать надежно защищенный путь поиска, чтобы злонамеренные пользователи не
     * могли перехватить управление.
     */
    res = PQexec(conn,
                 "SELECT pg_catalog.set_config('search_path', '', false)");
    if (PQresultStatus(res) != PGRES_TUPLES_OK)
    {
        fprintf(stderr, "SET failed: %s", PQerrorMessage(conn));
        PQclear(res);
        exit_nicely(conn);
    }
    PQclear(res);

    res = PQexec(conn, "begin");
    PQclear(res);
    printf("importing file \"%s\" ...\n", in_filename);
/*  lobjOid = importFile(conn, in_filename); */
    lobjOid = lo_import(conn, in_filename);
    if (lobjOid == 0)
        fprintf(stderr, "%s\n", PQerrorMessage(conn));
    else
    {
        printf("\tas large object %u.\n", lobjOid);

        printf("picking out bytes 1000-2000 of the large object\n");
        pickout(conn, lobjOid, 1000, 1000);

        printf("overwriting bytes 1000-2000 of the large object with X's\n");
        overwrite(conn, lobjOid, 1000, 1000);

        printf("exporting large object to file \"%s\" ...\n", out_filename);
/*      exportFile(conn, lobjOid, out_filename); */
        if (lo_export(conn, lobjOid, out_filename) < 0)
            fprintf(stderr, "%s\n", PQerrorMessage(conn));
    }

    res = PQexec(conn, "end");
    PQclear(res);
    PQfinish(conn);
    return 0;
}