Перейти к основному содержимому

Встраиваемая база данных из С

Разработчику Архитектору

Встраиваемая база данных из С

Встраиваемая СУБД — библиотека, которая работает внутри вашей программы, без отдельного сервера. Классический пример — SQLite: один файл app.db на диске, вызовы функций из C вместо сетевого протокола.

Общие понятия SQL (таблицы, SELECT, INSERT) — в разделе SQL. Здесь — как это выглядит в коде на С.


Термины API SQLite

ИмяРоль
sqlite3"соединение" с базой (один файл)
sqlite3_stmtподготовленный запрос с плейсхолдерами ?
sqlite3_stepвыполнить шаг (одна строка результата или завершение)
sqlite3_bind_*подставить значение вместо ?
sqlite3_column_*прочитать столбец текущей строки
SQLITE_OK, SQLITE_ROW, SQLITE_DONEкоды результата

Модель "библиотека в процессе"

В отличие от клиент-серверной PostgreSQL или MySQL:

  • нет отдельного демона;
  • нет сетевого порта;
  • запросы выполняются вызовами функций API;
  • файл .db — обычный файл в файловой системе.

Приложение линкуется с библиотекой (часто libsqlite3) и включает заголовок с объявлениями API.


Жизненный цикл соединения

Типичная последовательность:

  1. Открыть базу: указатель на соединение (sqlite3 *db).
  2. Выполнить SQL — создание таблиц, вставка, выборка.
  3. Закрыть соединение — освободить ресурсы.
#include <sqlite3.h>

int rc = sqlite3_open("app.db", &db);
if (rc != SQLITE_OK) {
fprintf(stderr, "open failed: %s\n", sqlite3_errmsg(db));
return 1;
}

/* ... работа ... */

sqlite3_close(db);

Разбор:

  • sqlite3_open("app.db", &db) открывает (или создаёт) файл базы и записывает дескриптор соединения в db.
  • Код rc сравнивается с SQLITE_OK, чтобы не продолжать выполнение с неинициализированным или аварийным соединением.
  • sqlite3_errmsg(db) возвращает человекочитаемое описание последней ошибки для текущего соединения.
  • sqlite3_close(db) закрывает соединение и освобождает внутренние ресурсы библиотеки.

sqlite3_open создаёт файл, если его нет (при успешных правах на каталог). Ошибки — кодами SQLITE_* и текстом sqlite3_errmsg(db).

Разбор проверки:

int rc = sqlite3_open("app.db", &db);
if (rc != SQLITE_OK) {
/* db может быть NULL или содержать сообщение */
fprintf(stderr, "%s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return 1;
}

Разбор:

  • Этот шаблон оформляет аварийную ветку сразу после открытия и не допускает дальнейших SQL-операций в ошибочном состоянии.
  • Вызов sqlite3_close(db) в ветке ошибки завершает частично созданное соединение и предотвращает утечки.
  • Возврат 1 фиксирует неуспешный статус процесса, что удобно для shell-скриптов и мониторинга.

Любой путь, где open не удался, должен вызывать sqlite3_close, чтобы не утекали дескрипторы.


Выполнение SQL

Простой путь — одна строка SQL и callback для каждой строки результата (sqlite3_exec). Подходит для CREATE TABLE, миграций, редких административных команд.

char *err = NULL;
const char *sql =
"CREATE TABLE IF NOT EXISTS users ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" name TEXT NOT NULL"
");";

if (sqlite3_exec(db, sql, NULL, NULL, &err) != SQLITE_OK) {
fprintf(stderr, "schema error: %s\n", err);
sqlite3_free(err);
return 1;
}

Разбор:

  • sqlite3_exec выполняет SQL целиком без отдельного stmt, что удобно для одноразовых DDL-команд.
  • Пятый аргумент &err получает текст ошибки; его нужно освобождать через sqlite3_free.
  • NULL, NULL для callback означает, что результат SELECT в этом вызове не обрабатывается построчно.

Подготовленные запросы (sqlite3_prepare_v2 + sqlite3_step) — основной путь для повторяющихся операций:

const char *sql = "INSERT INTO users (name, age) VALUES (?, ?);";
sqlite3_stmt *stmt;

if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK)
goto fail;

sqlite3_bind_text(stmt, 1, "Alice", -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 2, 30);

if (sqlite3_step(stmt) != SQLITE_DONE)
goto fail;

sqlite3_finalize(stmt);

Разбор:

  • sqlite3_prepare_v2 компилирует SQL в объект sqlite3_stmt, который можно исполнять и переиспользовать.
  • sqlite3_bind_text и sqlite3_bind_int безопасно подставляют параметры вместо ?, не склеивая строки вручную.
  • sqlite3_step(stmt) выполняет запрос; для INSERT успешное завершение ожидается как SQLITE_DONE.
  • sqlite3_finalize(stmt) обязателен: он освобождает ресурсы prepared statement на всех путях выполнения.

Плейсхолдеры ? или :name защищают от SQL-инъекций и переиспользуют план запроса.

Цикл чтения:

while (sqlite3_step(stmt) == SQLITE_ROW) {
const char *name = (const char *)sqlite3_column_text(stmt, 0);
int age = sqlite3_column_int(stmt, 1);
}

Разбор:

  • Цикл обрабатывает результат построчно: каждая итерация соответствует одной строке выборки.
  • sqlite3_column_text(stmt, 0) и sqlite3_column_int(stmt, 1) читают столбцы по нулевой индексации.
  • Доступ к column_* корректен только пока курсор находится на текущей строке; после следующего step данные могут измениться.
  • Такая модель позволяет обрабатывать большие выборки потоково, без загрузки всего результата в память.

Что возвращает sqlite3_step:

КодЗначение
SQLITE_ROWесть очередная строка результата — читайте column_*
SQLITE_DONEзапрос завершён (для INSERT/UPDATE без строк)
другоеошибка — смотрите sqlite3_errmsg

Индексы столбцов в column_* начинаются с 0. Строка из column_text действительна до следующего step на этом stmt — для долгого хранения скопируйте в свой буфер.

Плейсхолдеры: sqlite3_bind_text(stmt, 1, "Alice", -1, SQLITE_TRANSIENT) — номер 1 соответствует первому ? в SQL. Четвёртый аргумент -1 значит "длина по \0". SQLITE_TRANSIENT — SQLite скопирует строку сам.

Очистка stmt на всех путях:

Код ITЗагрузка примера кода…

Разбор:

  • stmt инициализируется NULL, чтобы в fail можно было безопасно вызвать finalize.
  • Метка fail гарантирует освобождение prepared statement даже при ошибке prepare или step.
  • Такой шаблон повторяет идиому goto cleanup из статьи про ошибки.

Транзакции

По умолчанию SQLite может автоматически фиксировать каждую операцию. Для пакетной вставки оборачивают в транзакцию:

BEGIN;
/* множество INSERT */
COMMIT;

Разбор:

  • BEGIN открывает транзакцию и объединяет группу изменений в одну атомарную операцию.
  • Пока транзакция не зафиксирована, изменения можно целиком отменить через ROLLBACK при любой ошибке в середине.
  • COMMIT завершает пакет и обычно резко сокращает накладные расходы на дисковую синхронизацию по сравнению с одиночными вставками.

При сбое — ROLLBACK. В API это те же строки SQL через exec или отдельные функции транзакций. Пакет в одной транзакции на порядки быстрее тысяч одиночных commit.


Типичные ошибки

ПроблемаПричина
database is lockedдругой поток/процесс держит запись; нужен таймаут или очередь
утечка stmtзабыли sqlite3_finalize
гонка при потокаходно соединение на поток или режим serialized + мьютекс
несовпадение типов bindbind_int и bind_text для столбца

См. Многопоточность на С: разделяемое соединение без синхронизации недопустимо.


Схема и миграции

Таблицы создают один раз при старте или версионируют схему:

CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);

Разбор:

  • CREATE TABLE IF NOT EXISTS создаёт таблицу только при отсутствии и безопасно проходит при повторном запуске.
  • Поле key TEXT PRIMARY KEY задаёт уникальный идентификатор записи и автоматически индексируется.
  • value TEXT NOT NULL запрещает пустое NULL-состояние и упрощает инварианты на уровне приложения.
  • Такой DDL-подход удобен для старта приложения: схема проверяется и приводится к нужному виду в рантайме.

Номер версии схемы хранят в таблице pragma user_version или отдельной таблице миграций; при обновлении приложения выполняют ALTER / пересоздание по скрипту.


Когда встраиваемая БД уместна

Подходит: локальное хранилище приложения, прототип, офлайн-режим, встраиваемые устройства, тестовые стенды.

Слабее: высокая конкурентная запись с множества сетевых клиентов, сложная репликация, тяжёлая аналитика на кластере — там серверные СУБД из обзора баз данных.


Связь с идиомами С

  • Проверять каждый код возврата API.
  • Освобождать stmt на всех путях (очистка с goto).
  • Не хранить указатели на column_text после следующего step без копирования строки.

См. также: Файловый ввод-вывод, Справочник.