Расширение Session
Содержание
1. Введение
Расширение Session предоставляет удобный механизм для записи изменений в таблицах базы данных SQLite, упаковки этих изменений в файл «набора изменений» или «набора исправлений» и последующего применения того же набора изменений к другой базе данных с той же схемой и совместимыми исходными данными. «Набор изменений» также можно инвертировать и использовать для отмены изменений сеанса.
Этот документ знакомит с расширением Session. Подробное описание интерфейса приведено в отдельном документе Интерфейс расширения Session на языке C.
1.1. Типичный сценарий использования
Предположим, что SQLite используется в качестве формата файлов приложения для определённого приложения для проектирования. Два пользователя, Алиса и Боб, начинают работу с исходным проектом размером около гигабайта. Весь день они параллельно работают над ним, каждый внося собственные изменения и доработки. В конце дня они хотят объединить свои изменения в единый проект.
Расширение Session упрощает эту задачу: оно записывает все изменения в базах данных Алисы и Боба и сохраняет их в файлы наборов изменений или наборов исправлений. В конце дня Алиса может отправить свой набор изменений Бобу, а Боб — «применить» его к своей базе данных. В результате (если конфликтов нет) база данных Боба будет содержать и его изменения, и изменения Алисы. Аналогично Боб может отправить Алисе набор изменений со своей работой, а она — применить их к своей базе данных.
Иными словами, расширение Session предоставляет для файлов баз данных SQLite возможности, похожие на возможности утилиты patch в Unix или функции «слияния» систем контроля версий, таких как Fossil, Git или Mercurial.
1.2. Получение расширения Session
Начиная с версии 3.13.0 (2016-05-18), расширение Session входит в дистрибутив исходного кода SQLite в виде амальгамации. По умолчанию расширение Session отключено. Чтобы включить его, выполните сборку со следующими параметрами компилятора:
-DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_PREUPDATE_HOOK
Или, если используется каноническая система сборки, передайте скрипту configure параметр --enable-session.
1.3. Ограничения
До версии SQLite 3.17.0 расширение Session работало только с таблицами с rowid, но не с таблицами WITHOUT ROWID. Начиная с версии 3.17.0 поддерживаются оба типа таблиц. Однако для записи изменений в таблицах WITHOUT ROWID требуются дополнительные действия по сохранению первичных ключей.
Поддержка виртуальных таблиц отсутствует. Изменения в виртуальных таблицах не записываются.
Расширение Session работает только с таблицами, для которых объявлен PRIMARY KEY. PRIMARY KEY таблицы может быть INTEGER PRIMARY KEY (псевдоним rowid) или внешним PRIMARY KEY.
SQLite позволяет хранить значения NULL в столбцах PRIMARY KEY. Однако расширение Session игнорирует все такие строки. Модуль сеансов не записывает изменения, затрагивающие строки с одним или несколькими значениями NULL в столбцах PRIMARY KEY.
2. Основные понятия
2.1. Наборы изменений и наборы исправлений
Работа модуля сеансов основана на создании наборов изменений и манипуляциях с ними. Набор изменений — это блок данных, кодирующий последовательность изменений базы данных. Каждое изменение в наборе относится к одному из следующих типов:
-
INSERT. Изменение INSERT содержит одну строку, которую нужно добавить в таблицу базы данных. Полезная нагрузка изменения INSERT состоит из значений каждого поля новой строки.
-
DELETE. Изменение DELETE представляет строку, которую нужно удалить из таблицы базы данных. Строка определяется значениями первичного ключа. Полезная нагрузка изменения DELETE состоит из значений всех полей удалённой строки.
-
UPDATE. Изменение UPDATE представляет собой изменение одного или нескольких полей строки базы данных, не входящих в PRIMARY KEY. Строка определяется полями PRIMARY KEY. Полезная нагрузка изменения UPDATE состоит из:
- значений PRIMARY KEY, идентифицирующих изменённую строку,
- новых значений каждого изменённого поля строки и
- исходных значений каждого изменённого поля строки.
Изменение UPDATE не содержит сведений о полях, не входящих в PRIMARY KEY, которые это изменение не затрагивает. Изменение UPDATE не может задавать изменения полей PRIMARY KEY.
Один набор изменений может содержать изменения для нескольких таблиц базы данных. Для каждой таблицы, для которой в наборе записано хотя бы одно изменение, также кодируются следующие данные:
- имя таблицы базы данных,
- число столбцов таблицы и
- указание на то, какие из этих столбцов являются столбцами PRIMARY KEY.
Наборы изменений можно применять только к базам данных, содержащим таблицы, соответствующие всем трём указанным критериям, сохранённым в наборе изменений.
Набор исправлений похож на набор изменений. Он немного компактнее, но предоставляет более ограниченные возможности обнаружения и разрешения конфликтов (подробнее см. следующий раздел). Набор исправлений отличается от набора изменений следующим:
Для изменения DELETE полезная нагрузка содержит только поля PRIMARY KEY. Исходные значения остальных полей не сохраняются в наборе исправлений.
Для изменения UPDATE полезная нагрузка содержит только поля PRIMARY KEY и новые значения изменённых полей. Исходные значения изменённых полей не сохраняются в наборе исправлений.
2.2. Конфликты
При применении набора изменений или набора исправлений к базе данных выполняется попытка добавить новую строку для каждого изменения INSERT, удалить строку для каждого изменения DELETE и изменить строку для каждого изменения UPDATE. Если целевая база данных находится в том же состоянии, что и исходная база данных, в которой был записан набор изменений, это не представляет сложности. Однако если содержимое целевой базы данных отличается от этого состояния, при применении набора изменений или набора исправлений могут возникнуть конфликты.
При обработке изменения INSERT могут возникнуть следующие конфликты:
- Целевая база данных уже может содержать строку с теми же значениями PRIMARY KEY, что и указанные в изменении INSERT.
- При добавлении новой строки может быть нарушено другое ограничение базы данных, например UNIQUE или CHECK.
При обработке изменения DELETE могут быть обнаружены следующие конфликты:
- В целевой базе данных может не быть строки с указанными значениями PRIMARY KEY для удаления.
- Целевая база данных может содержать строку с указанными значениями PRIMARY KEY, но значения остальных полей могут не совпадать со значениями, сохранёнными в наборе изменений. Такой тип конфликта не обнаруживается при использовании набора исправлений.
При обработке изменения UPDATE могут быть обнаружены следующие конфликты:
- В целевой базе данных может не быть строки с указанными значениями PRIMARY KEY для изменения.
- Целевая база данных может содержать строку с указанными значениями PRIMARY KEY, но текущие значения полей, которые будут изменены, могут не совпадать с исходными значениями, сохранёнными в наборе изменений. Такой тип конфликта не обнаруживается при использовании набора исправлений.
- При обновлении строки может быть нарушено другое ограничение базы данных, например UNIQUE или CHECK.
В зависимости от типа конфликта приложение сеансов может использовать различные настраиваемые способы обработки конфликтов: от пропуска конфликтующего изменения до отмены применения всего набора изменений или применения изменения вопреки конфликту. Подробности см. в документации API sqlite3changeset_apply().
2.3. Формирование набора изменений
После настройки объекта сеанса он начинает отслеживать изменения в настроенных таблицах. Однако при каждом изменении строки в базе данных он не записывает изменение целиком. Вместо этого для каждой добавленной строки записываются только поля PRIMARY KEY, а для изменённых или удалённых строк — только PRIMARY KEY и все исходные значения полей. Если одна строка изменяется несколько раз в рамках одного сеанса, новые сведения не записываются.
Остальные сведения, необходимые для создания набора изменений или набора исправлений, считываются из файла базы данных при вызове sqlite3session_changeset() или sqlite3session_patchset(). В частности:
-
Для каждого первичного ключа, записанного в результате операции INSERT, модуль сеансов проверяет, осталась ли в таблице строка с соответствующим первичным ключом. Если да, в набор изменений добавляется изменение INSERT.
-
Для каждого первичного ключа, записанного в результате операции UPDATE или DELETE, модуль сеансов также проверяет, есть ли в таблице строка с соответствующим первичным ключом. Если такая строка найдена, но значение одного или нескольких полей, не входящих в PRIMARY KEY, не совпадает с исходным записанным значением, в набор изменений добавляется UPDATE. Если строки с указанным первичным ключом нет, в набор изменений добавляется DELETE. Если строка существует, но ни одно из полей, не входящих в PRIMARY KEY, не было изменено, в набор изменений ничего не добавляется.
Из этого следует, что если изменение сначала вносится, а затем отменяется в рамках одного сеанса (например, строка добавляется, а затем удаляется), модуль сеансов не сообщает ни об одном изменении. Если же строка несколько раз обновляется в рамках одного сеанса, все обновления объединяются в одно изменение UPDATE в любом блоке набора изменений или набора исправлений.
3. Использование расширения Session
В этом разделе приведены примеры использования расширения Session.
3.1. Сбор набора изменений
В приведённом ниже примере кода показаны действия, необходимые для сбора набора изменений при выполнении команд SQL. Кратко:
-
Объект сеанса (типа sqlite3_session*) создаётся вызовом функции API sqlite3session_create().
Один объект сеанса отслеживает изменения в одной базе данных (то есть «main», «temp» или присоединённой базе данных) с помощью одного дескриптора базы данных sqlite3*.
-
Объект сеанса настраивается для отслеживания изменений в наборе таблиц.
По умолчанию объект сеанса не отслеживает изменения ни в одной таблице базы данных. Перед началом отслеживания его необходимо настроить. Существует три способа задать набор таблиц для отслеживания изменений:
- Явно указать таблицы, вызвав sqlite3session_attach() отдельно для каждой таблицы; или
- Указать, что необходимо отслеживать изменения во всех таблицах базы данных, вызвав sqlite3session_attach() с аргументом NULL; или
- Настроить обратный вызов, который вызывается при первой записи в каждую таблицу и сообщает модулю сеансов, нужно ли отслеживать изменения в этой таблице.
В приведённом ниже примере кода используется второй из перечисленных способов — отслеживаются изменения во всех таблицах базы данных.
-
В базу данных вносятся изменения путём выполнения операторов SQL. Объект сеанса записывает эти изменения.
-
Блок набора изменений извлекается из объекта сеанса вызовом sqlite3session_changeset() (или, при использовании наборов исправлений, вызовом функции sqlite3session_patchset()).
-
Объект сеанса удаляется вызовом функции API sqlite3session_delete().
Удалять объект сеанса после извлечения из него набора изменений или набора исправлений необязательно. Его можно оставить подключённым к дескриптору базы данных; он продолжит отслеживать изменения в настроенных таблицах, как и прежде. Однако если вызвать sqlite3session_changeset() или sqlite3session_patchset() для объекта сеанса во второй раз, набор изменений или набор исправлений будет содержать все изменения, произошедшие в соединении с момента создания сеанса. Иными словами, вызов sqlite3session_changeset() или sqlite3session_patchset() не сбрасывает и не обнуляет объект сеанса.
/*
** Argument zSql points to a buffer containing an SQL script to execute
** against the database handle passed as the first argument. As well as
** executing the SQL script, this function collects a changeset recording
** all changes made to the "main" database file. Assuming no error occurs,
** output variables (*ppChangeset) and (*pnChangeset) are set to point
** to a buffer containing the changeset and the size of the changeset in
** bytes before returning SQLITE_OK. In this case it is the responsibility
** of the caller to eventually free the changeset blob by passing it to
** the sqlite3_free function.
**
** Or, if an error does occur, return an SQLite error code. The final
** value of (*pChangeset) and (*pnChangeset) are undefined in this case.
*/
int sql_exec_changeset(
sqlite3 *db, /* Database handle */
const char *zSql, /* SQL script to execute */
int *pnChangeset, /* OUT: Size of changeset blob in bytes */
void **ppChangeset /* OUT: Pointer to changeset blob */
){
sqlite3_session *pSession = 0;
int rc;
/* Create a new session object */
rc = sqlite3session_create(db, "main", &pSession);
/* Configure the session object to record changes to all tables */
if( rc==SQLITE_OK ) rc = sqlite3session_attach(pSession, NULL);
/* Execute the SQL script */
if( rc==SQLITE_OK ) rc = sqlite3_exec(db, zSql, 0, 0, 0);
/* Collect the changeset */
if( rc==SQLITE_OK ){
rc = sqlite3session_changeset(pSession, pnChangeset, ppChangeset);
}
/* Delete the session object */
sqlite3session_delete(pSession);
return rc;
}
3.2. Применение набора изменений к базе данных
Применить набор изменений к базе данных проще, чем собрать его. Обычно достаточно одного вызова sqlite3changeset_apply(), как показано в примере кода ниже.
Если применение набора изменений оказывается сложным, трудности связаны с разрешением конфликтов. Подробности см. в документации API по ссылке выше.
/*
** Conflict handler callback used by apply_changeset(). See below.
*/
static int xConflict(void *pCtx, int eConflict, sqlite3_changeset_iter *pIter){
int ret = (int)pCtx;
return ret;
}
/*
** Apply the changeset contained in blob pChangeset, size nChangeset bytes,
** to the main database of the database handle passed as the first argument.
** Return SQLITE_OK if successful, or an SQLite error code if an error
** occurs.
**
** If parameter bIgnoreConflicts is true, then any conflicting changes
** within the changeset are simply ignored. Or, if bIgnoreConflicts is
** false, then this call fails with an SQLITE_ABORT error if a changeset
** conflict is encountered.
*/
int apply_changeset(
sqlite3 *db, /* Database handle */
int bIgnoreConflicts, /* True to ignore conflicting changes */
int nChangeset, /* Size of changeset in bytes */
void *pChangeset /* Pointer to changeset blob */
){
return sqlite3changeset_apply(
db,
nChangeset, pChangeset,
0, xConflict,
(void*)bIgnoreConflicts
);
}
3.3. Просмотр содержимого набора изменений
В приведённом ниже примере кода показаны способы перебора и извлечения данных обо всех изменениях в наборе изменений. Кратко:
Вызывается API sqlite3changeset_start() для создания и инициализации итератора, который будет перебирать содержимое набора изменений. Изначально итератор не указывает ни на один элемент.
Первый вызов sqlite3changeset_next() для итератора перемещает его к первому изменению в наборе изменений (или к EOF, если набор изменений полностью пуст). sqlite3changeset_next() возвращает SQLITE_ROW, если итератор перемещён к допустимой записи, SQLITE_DONE — если он перемещён к EOF, или код ошибки SQLite, если произошла ошибка.
Если итератор указывает на допустимую запись, API sqlite3changeset_op() можно использовать, чтобы определить тип изменения (INSERT, UPDATE или DELETE), на которое указывает итератор. Кроме того, тот же API позволяет получить имя таблицы, к которой относится изменение, ожидаемое число её столбцов и число столбцов первичного ключа.
Если итератор указывает на допустимую запись INSERT или UPDATE, API sqlite3changeset_new() можно использовать для получения значений new.* из полезной нагрузки изменения.
Если итератор указывает на допустимую запись DELETE или UPDATE, API sqlite3changeset_old() можно использовать для получения значений old.* из полезной нагрузки изменения.
Итератор удаляется вызовом API sqlite3changeset_finalize(). Если во время перебора произошла ошибка, возвращается код ошибки SQLite (даже если тот же код ошибки уже был возвращён функцией sqlite3changeset_next()). Если ошибки не было, возвращается SQLITE_OK.
/*
** Print the contents of the changeset to stdout.
*/
static int print_changeset(void *pChangeset, int nChangeset){
int rc;
sqlite3_changeset_iter *pIter = 0;
/* Create an iterator to iterate through the changeset */
rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset);
if( rc!=SQLITE_OK ) return rc;
/* This loop runs once for each change in the changeset */
while( SQLITE_ROW==sqlite3changeset_next(pIter) ){
const char *zTab; /* Table change applies to */
int nCol; /* Number of columns in table zTab */
int op; /* SQLITE_INSERT, UPDATE or DELETE */
sqlite3_value *pVal;
/* Print the type of operation and the table it is on */
rc = sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0);
if( rc!=SQLITE_OK ) goto exit_print_changeset;
printf("%s on table %s\n",
op==SQLITE_INSERT?"INSERT" : op==SQLITE_UPDATE?"UPDATE" : "DELETE",
zTab
);
/* If this is an UPDATE or DELETE, print the old.* values */
if( op==SQLITE_UPDATE || op==SQLITE_DELETE ){
printf("Old values:");
for(i=0; i<nCol; i++){
rc = sqlite3changeset_old(pIter, i, &pVal);
if( rc!=SQLITE_OK ) goto exit_print_changeset;
printf(" %s", pVal ? sqlite3_value_text(pVal) : "-");
}
printf("\n");
}
/* If this is an UPDATE or INSERT, print the new.* values */
if( op==SQLITE_UPDATE || op==SQLITE_INSERT ){
printf("New values:");
for(i=0; i<nCol; i++){
rc = sqlite3changeset_new(pIter, i, &pVal);
if( rc!=SQLITE_OK ) goto exit_print_changeset;
printf(" %s", pVal ? sqlite3_value_text(pVal) : "-");
}
printf("\n");
}
}
/* Clean up the changeset and return an error code (or SQLITE_OK) */
exit_print_changeset:
rc2 = sqlite3changeset_finalize(pIter);
if( rc==SQLITE_OK ) rc = rc2;
return rc;
}
4. Расширенные возможности
Большинство приложений будут использовать только возможности модуля сеансов, описанные в предыдущем разделе. Однако для работы с блоками наборов изменений и наборов исправлений доступны следующие дополнительные возможности:
-
Два или более набора изменений/наборов исправлений можно объединить с помощью интерфейсов sqlite3changeset_concat() или sqlite3_changegroup.
-
Набор изменений можно «инвертировать» с помощью функции API sqlite3changeset_invert(). Инвертированный набор изменений отменяет изменения, внесённые исходным набором. Если набор изменений C+ является обратным набору изменений C, то применение к базе данных сначала C, а затем C+ должно оставить базу данных без изменений.
Последнее обновление этой страницы: 2025-05-31 13:08:22Z
SQLite is in the Public Domain.
https://sqlite.org/sessionintro.html