SQL и Qt

SQLite: встраиваемая БД, особенности и использование с Qt (QSqlDatabase)

Введение

SQLite принципиально отличается от PostgreSQL/MySQL (статьи 423-424) — это не отдельный серверный процесс, к которому приложение подключается по сети, а встраиваемая библиотека, работающая непосредственно в процессе самого приложения, с данными, хранящимися в единственном файле на диске. В статье разберём особенности SQLite и его практическое использование из Qt-приложений через QSqlDatabase, продолжая тему статьи 24 основного цикла (БД/ORM) и Raspberry Pi/embedded-циклов, где SQLite — частый, естественный выбор.

Концепция

SQLite не требует отдельного серверного процесса, конфигурации сети, или управления пользователями/правами доступа (статья 434) — вся БД представляет собой единственный файл, который приложение открывает напрямую, что делает SQLite идеальным выбором для desktop/мобильных приложений, встраиваемых систем (Raspberry Pi-цикл), и локального тестирования (статья 439), но менее подходящим для высоконагруженных, многопользовательских серверных приложений с интенсивной конкурентной записью (где SQLite использует блокировку на уровне всей БД для записи, что ограничивает параллелизм значительно сильнее, чем построчные блокировки PostgreSQL/MySQL InnoDB, статьи 409, 424).

Пример кода: SQLite

// Подключение к SQLite через QSqlDatabase в Qt-приложении
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>

bool initializeDatabase(const QString &filePath)
{
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    db.setDatabaseName(filePath);

    if (!db.open()) {
        qWarning() << "Не удалось открыть БД:" << db.lastError().text();
        return false;
    }

    QSqlQuery query;
    query.exec("CREATE TABLE IF NOT EXISTS readings ("
               "id INTEGER PRIMARY KEY AUTOINCREMENT, "
               "device_id TEXT NOT NULL, "
               "temperature REAL, "
               "recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP)");

    return true;
}
// Параметризованный запрос через QSqlQuery — защита от SQL-инъекций (статья 433)
void insertReading(const QString &deviceId, double temperature)
{
    QSqlQuery query;
    query.prepare("INSERT INTO readings (device_id, temperature) VALUES (:device_id, :temperature)");
    query.bindValue(":device_id", deviceId);
    query.bindValue(":temperature", temperature);

    if (!query.exec()) {
        qWarning() << "Ошибка вставки:" << query.lastError().text();
    }
}
// Включение WAL-режима (Write-Ahead Logging) — улучшает параллелизм чтения/записи SQLite
void enableWalMode(QSqlDatabase &db)
{
    QSqlQuery query(db);
    query.exec("PRAGMA journal_mode=WAL;");
    // WAL позволяет читателям не блокироваться на время записи (в отличие от стандартного
    // режима журналирования), что особенно полезно для приложений с конкурентным чтением/записью
}

Пояснения к коду

QSqlDatabase::addDatabase("QSQLITE") показывает, что с точки зрения Qt-приложения API для работы с SQLite почти не отличается от работы с PostgreSQL/MySQL через тот же QSqlDatabase — основное отличие лишь в драйвере ("QSQLITE" вместо "QPSQL"/"QMYSQL") и в setDatabaseName(), принимающем путь к файлу, а не имя БД на удалённом сервере. query.prepare()/bindValue() показывает параметризованный запрос (статья 433) — критично важная практика для защиты от SQL-инъекций, применимая одинаково независимо от конкретной используемой СУБД. PRAGMA journal_mode=WAL — специфичная для SQLite настройка, заметно улучшающая параллелизм между чтением и записью по сравнению со стандартным режимом журналирования, что особенно ценно для embedded-приложений (Raspberry Pi-цикл), где несколько потоков/процессов могут одновременно обращаться к одному файлу БД.

Подводные камни

  • Использование SQLite для высоконагруженного, многопользовательского серверного приложения с интенсивной конкурентной записью, где более подходящим выбором были бы PostgreSQL/MySQL — блокировка на уровне всей БД при записи (даже с WAL-режимом, существенно улучшающим, но не полностью устраняющим это ограничение) делает SQLite принципиально менее подходящим для сценариев с большим количеством одновременных, конкурирующих операций записи от множества разных клиентов, в отличие от desktop/мобильных/embedded-приложений с единственным процессом, обращающимся к БД.
  • Открытие одного файла SQLite БД одновременно несколькими отдельными процессами на платформах/файловых системах с проблемной поддержкой файловых блокировок (некоторые сетевые файловые системы, особенно при доступе с нескольких разных машин одновременно) — SQLite опирается на корректную работу файловых блокировок операционной системы для предотвращения повреждения данных при конкурентном доступе, и на файловых системах, где эти блокировки работают ненадёжно, конкурентный доступ к одному файлу SQLite может привести к повреждению данных.
  • Забытое включение поддержки внешних ключей — в отличие от PostgreSQL/MySQL (с InnoDB), SQLite по умолчанию НЕ применяет ограничения внешних ключей (статья 397), даже если они явно объявлены в схеме, если не выполнена отдельная команда PRAGMA foreign_keys = ON; для конкретного соединения — без этой явной настройки определения внешних ключей в схеме существуют лишь как документация, не давая реальной защиты ссылочной целостности.
  • Хранение файла БД SQLite в директории, синхронизируемой облачным сервисом (Dropbox, OneDrive, Google Drive и аналогичные) для desktop-приложения — синхронизация файла БД во время активной записи может привести к повреждению файла (синхронизация частично записанного, незавершённого файла) или конфликтам при попытке одновременного изменения с разных синхронизированных устройств; файл БД SQLite, активно используемый приложением, не должен находиться в директории такой автоматической синхронизации без явного понимания и принятия этого риска.