Встраиваемая база данных из С
Встраиваемая база данных из С
Встраиваемая СУБД — библиотека, которая работает внутри вашей программы, без отдельного сервера. Классический пример — 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.
Жизненный цикл соединения
Типичная последовательность:
- Открыть базу: указатель на соединение (
sqlite3 *db). - Выполнить SQL — создание таблиц, вставка, выборка.
- Закрыть соединение — освободить ресурсы.
#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 + мьютекс |
| несовпадение типов bind | bind_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без копирования строки.
См. также: Файловый ввод-вывод, Справочник.