Как работать с транзакциями в SQL из C++ приложения?

«Как работать с транзакциями в SQL из C++ приложения?» — вопрос из категории Базы данных, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В C++ приложениях для работы с SQL-транзакциями используются библиотеки-коннекторы (например, libpqxx для PostgreSQL, MySQL Connector/C++, SQLite C/C++ Interface или абстракции вроде ODBC, Qt SQL). Общий паттерн работы с транзакцией следующий:

  1. Начало транзакции: Явный вызов BEGIN или START TRANSACTION. Многие библиотеки предоставляют объект Transaction, конструктор которого автоматически начинает транзакцию.
  2. Выполнение операций: Выполнение одного или нескольких SQL-запросов (INSERT, UPDATE, DELETE, SELECT) в рамках этой транзакции.
  3. Фиксация или откат: Если все операции успешны — выполняется COMMIT, изменения сохраняются в БД. Если произошла ошибка — выполняется ROLLBACK, все изменения в рамках транзакции отменяются.

Пример с использованием библиотеки SQLite (C API):

#include <sqlite3.h>
#include <iostream>

int main() {
    sqlite3* db;
    char* errMsg = nullptr;

    sqlite3_open(":memory:", &db); // Открываем БД в памяти.

    // 1. Явно начинаем транзакцию.
    sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, &errMsg);

    // 2. Выполняем операции.
    bool success = true;
    if (sqlite3_exec(db, "CREATE TABLE test(id INT);", nullptr, nullptr, &errMsg) != SQLITE_OK) {
        success = false;
    }
    if (success && sqlite3_exec(db, "INSERT INTO test VALUES (1);", nullptr, nullptr, &errMsg) != SQLITE_OK) {
        success = false;
    }

    // 3. Фиксация или откат.
    if (success) {
        sqlite3_exec(db, "COMMIT;", nullptr, nullptr, &errMsg);
        std::cout << "Transaction committed.n";
    } else {
        std::cerr << "Error: " << errMsg << " -> Rolling back.n";
        sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr);
        sqlite3_free(errMsg);
    }
    sqlite3_close(db);
    return 0;
}

Пример с использованием более современной обёртки (SQLiteCpp):

#include <SQLiteCpp/SQLiteCpp.h>
#include <iostream>

try {
    SQLite::Database db(":memory:", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
    // Объект Transaction автоматически выполняет BEGIN в конструкторе.
    SQLite::Transaction transaction(db);

    db.exec("CREATE TABLE test (id INTEGER)");
    db.exec("INSERT INTO test VALUES (42)");

    // commit() автоматически вызывается в деструкторе, если не вызван rollback().
    transaction.commit();
    std::cout << "Transaction committed successfully.n";
} catch (const std::exception& e) {
    // При выбросе исключения transaction будет уничтожен, и произойдёт автоматический rollback.
    std::cerr << "SQLite error: " << e.what() << std::endl;
}

Ключевые моменты:

  • Режим автокоммита обычно включён по умолчанию. При явном начале транзакции он отключается.
  • Долгие транзакции могут удерживать блокировки и негативно влиять на параллельную работу.
  • Уровни изоляции транзакций (Read Committed, Repeatable Read и др.) позволяют управлять видимостью изменений между параллельными транзакциями и настраиваются через SQL-команды (например, SET TRANSACTION ISOLATION LEVEL ...).