QtとSQLiteによるデータベースプログラミング

1. 単一テーブルの基本的なデータベース操作

QtフレームワークのSQLモジュールは、SQLite、MySQL、PostgreSQLなど、様々なデータベースとの連携を可能にします。ここでは、QtのQSQLITEドライバーを使用して、単一のデータベースファイルとテーブルを扱う基本的な手順を説明します。

データベース操作のフロー

  1. データベース接続を確立し、オープンする。
  2. データベース内にテーブルが存在しない場合、作成する。
  3. テーブルからデータを取得し、結果を反復処理する。
  4. データベース接続をクローズする。

以下のC++コードは、SQLiteデータベースファイル app_records.db を作成し、blast_events というテーブルを管理する例です。テーブルが存在しない場合は新しく作成し、その後データをクエリして結果を処理します。

// データベース接続の確立とオープン
QSqlDatabase dbConnection = QSqlDatabase::addDatabase("QSQLITE");
dbConnection.setDatabaseName("app_records.db"); // データベースファイル名を設定

if (!dbConnection.open()) {
    qDebug() << "エラー: データベースへの接続に失敗しました。" << dbConnection.lastError();
    // ここでアプリケーションの終了など、適切なエラー処理を行う
    return;
}

// QSqlQueryオブジェクトの準備
QSqlQuery dbQuery(dbConnection);

// テーブルの作成(存在しない場合)
QString createTableSql = "CREATE TABLE IF NOT EXISTS blast_events ("
                         "id INTEGER PRIMARY KEY AUTOINCREMENT, "
                         "event_date TEXT, "
                         "event_count INTEGER, "
                         "status_code INTEGER, "
                         "record_id INTEGER)";
if (!dbQuery.exec(createTableSql)) {
    qDebug() << "エラー: テーブル 'blast_events' の作成に失敗しました。" << dbQuery.lastError();
} else {
    qDebug() << "テーブル 'blast_events' が準備できました。";
}

// データの挿入例 (必要に応じてコメント解除して使用)
/*
dbQuery.prepare("INSERT INTO blast_events (event_date, event_count, status_code, record_id) VALUES (?, ?, ?, ?)");
dbQuery.bindValue(0, "2023-11-01");
dbQuery.bindValue(1, 10);
dbQuery.bindValue(2, 0); // ステータスコード
dbQuery.bindValue(3, 201);
if (!dbQuery.exec()) {
    qDebug() << "データの挿入に失敗しました。" << dbQuery.lastError();
} else {
    qDebug() << "新しいレコードが挿入されました。";
}
*/

// テーブルからのデータ読み込みと処理
int inactiveRecordsCount = 0;
if (dbQuery.exec("SELECT id, event_date, event_count, status_code, record_id FROM blast_events")) {
    while (dbQuery.next()) {
        int recordId = dbQuery.value("id").toInt();
        QString date = dbQuery.value("event_date").toString();
        int count = dbQuery.value("event_count").toInt();
        int status = dbQuery.value("status_code").toInt();
        int associatedRecordId = dbQuery.value("record_id").toInt();

        // status_code が 1 ではないレコードを特定
        if (status != 1) {
            inactiveRecordsCount++;
            qDebug() << "非アクティブなレコードが見つかりました: ID=" << recordId
                     << ", 日付=" << date << ", ステータス=" << status;
        }
        // ここで取得したデータをアプリケーションのロジックで利用できます
    }
    qDebug() << "処理された非アクティブなレコードの総数: " << inactiveRecordsCount;
} else {
    qDebug() << "エラー: データ取得に失敗しました。" << dbQuery.lastError();
}

// データベース接続のクローズ
dbConnection.close();
qDebug() << "データベース接続がクローズされました。";

2. 複数のテーブルを管理するデータベース設計

実際のアプリケーションでは、単一のデータベースファイル内に複数のテーブルを持つことが一般的です。これを効果的に管理するために、データベース接続とテーブル作成ロジックをカプセル化したシングルトンデザインパターンを持つクラスを作成することが推奨されます。

以下の例では、DatabaseManager というクラスがデータベースへの単一のエントリポイントを提供し、アプリケーション起動時に必要なすべてのテーブルが存在することを確認します。これにより、複数の箇所でデータベース接続やテーブル作成ロジックを重複させることを避け、保守性を向上させます。

#include <QSqlRecord>
#include <QSqlDriver>
#include <QSqlField>
#include <QMutex>
#include <QDebug>
#include <QStringList> // QStringList が必要

// データベース接続設定用の定数
#define DB_CONNECTION_NAME "application_db_session"
#define DB_FILE_NAME "application_data.db"
#define DB_DRIVER "QSQLITE"

// データベースを管理するシングルトンクラス
class DatabaseManager : public QObject
{
    Q_OBJECT
public:
    static DatabaseManager* instance(); // シングルトンインスタンスを取得
    ~DatabaseManager(); // デストラクタ

    QSqlDatabase getDatabase() const { return m_database; } // データベース接続オブジェクトを返す

    // 雷管メッセージ構造体(元のコードのLGMessageDefを模倣)
    struct DetonatorMessage {
        long long Uid;        // ユニークID
        int DT;               // 遅延時間 (Delay Time)
        int KW;               // キーワード (エリアとホール情報を含む)
        int Det_status;       // 雷管ステータスフラグ
        int DesignDT;         // 設計遅延時間
        int DesignKW;         // 設計キーワード
    };
    // 雷管ステータスフラグの定義
    enum DetonatorStatusFlags {
        DesignFlag = 0x01,    // 設計済み
        AuthFlag = 0x02       // 認証済み
    };

    // 雷管パラメータを処理し、データベースに保存するメソッド
    void processDetonatorParameters(DetonatorMessage* messages, int count);

private:
    explicit DatabaseManager(QObject *parent = nullptr); // コンストラクタ
    static DatabaseManager* m_instance; // シングルトンインスタンス
    static QMutex m_mutex;             // スレッドセーフのためのミューテックス
    QSqlDatabase m_database;            // データベース接続オブジェクト

    // ヘルパー関数: UIDを文字列に変換
    QString convertDetonatorIdToString(long long uid);

    // ヘルパー関数: テーブルが存在しない場合に作成
    void ensureTableExists(QSqlQuery& query, const QString& tableName, const QString& createSql);
};

// シングルトンインスタンスの初期化
DatabaseManager* DatabaseManager::m_instance = nullptr;
QMutex DatabaseManager::m_mutex;

// シングルトンインスタンスを取得する静的メソッド
DatabaseManager* DatabaseManager::instance()
{
    if (!m_instance) {
        QMutexLocker locker(&m_mutex); // スレッドセーフなインスタンス生成
        if (!m_instance) {
            m_instance = new DatabaseManager();
        }
    }
    return m_instance;
}

// コンストラクタ: データベース接続を確立し、必要なテーブルを作成
DatabaseManager::DatabaseManager(QObject *parent) : QObject(parent)
{
    if (QSqlDatabase::contains(DB_CONNECTION_NAME)) {
        m_database = QSqlDatabase::database(DB_CONNECTION_NAME);
    } else {
        m_database = QSqlDatabase::addDatabase(DB_DRIVER, DB_CONNECTION_NAME);
        m_database.setDatabaseName(DB_FILE_NAME);
    }

    if (!m_database.isOpen()) {
        if (!m_database.open()) {
            qDebug() << "エラー: データベースのオープンに失敗しました。" << m_database.lastError();
            // 致命的なエラーのため、適切なエラーハンドリングが必要
        }
    }

    QSqlQuery dbQuery(m_database);

    // 各テーブルが存在するか確認し、なければ作成する
    ensureTableExists(dbQuery, "system_configurations",
        "CREATE TABLE system_configurations(id INTEGER PRIMARY KEY, brightness INTEGER, backlight INTEGER, timestamp DATETIME, "
        "longitude VARCHAR(5), latitude VARCHAR(5), location_time DATETIME, style INTEGER, key_sound INTEGER, charge_level INTEGER, "
        "can_switch INTEGER, mA1_data INTEGER, mA2_data INTEGER, uA_data INTEGER, LG00_enabled INTEGER, org_number INTEGER, "
        "status_check INTEGER, page_style INTEGER, needs_auth INTEGER, needs_gps INTEGER, auth_number INTEGER, "
        "device_id1 INTEGER, device_id2 INTEGER, device_id3 INTEGER, device_id4 INTEGER, device_id5 INTEGER, "
        "software_version INTEGER, device_status INTEGER)");

    ensureTableExists(dbQuery, "detonator_delay_settings",
        "CREATE TABLE detonator_delay_settings(id INTEGER PRIMARY KEY AUTOINCREMENT, detonator_code TEXT UNIQUE, delay_time INTEGER, "
        "area_id INTEGER, hole_id INTEGER, auth_status INTEGER)");

    ensureTableExists(dbQuery, "event_logs",
        "CREATE TABLE event_logs(id INTEGER PRIMARY KEY AUTOINCREMENT, event_date TEXT, "
        "count INTEGER, status INTEGER, blast_record_id INTEGER, longitude TEXT, latitude TEXT, "
        "bscan_mA INTEGER, bscan_mV INTEGER, scanning_mA INTEGER, scanning_mV INTEGER, "
        "bcharge_mA INTEGER, bcharge_mV INTEGER, charging_mA INTEGER, charging_mV INTEGER, "
        "charged_mA INTEGER, charged_mV INTEGER, bboom_mA INTEGER, bboom_mV INTEGER, bat_voltage INTEGER)");

    ensureTableExists(dbQuery, "record_identifiers",
        "CREATE TABLE record_identifiers(id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT, time INTEGER, blast_record_id INTEGER)");

    ensureTableExists(dbQuery, "authorization_headers",
        "CREATE TABLE authorization_headers(id INTEGER PRIMARY KEY AUTOINCREMENT, esp_data TEXT, "
        "auth_id TEXT, longitude VARCHAR(5), latitude VARCHAR(5), "
        "radius INTEGER, start_time TEXT, stop_time TEXT, lg_number INTEGER, "
        "useful_time INTEGER, lg_range_number INTEGER, password_number INTEGER, download_data TEXT)");

    ensureTableExists(dbQuery, "authorization_passwords",
        "CREATE TABLE authorization_passwords(id INTEGER PRIMARY KEY AUTOINCREMENT, lg_password TEXT, auth_file_id INTEGER)");

    ensureTableExists(dbQuery, "user_accounts",
        "CREATE TABLE user_accounts(user_id INTEGER PRIMARY KEY AUTOINCREMENT, org_id VARCHAR(32), "
        "real_name NVARCHAR(32), org_name NVARCHAR(32), login_name VARCHAR(32), user_password VARCHAR(32))");

    qDebug() << "データベースマネージャーが初期化されました。";
}

// デストラクタ: データベース接続をクローズ
DatabaseManager::~DatabaseManager()
{
    if (m_database.isOpen()) {
        m_database.close();
        QSqlDatabase::removeDatabase(DB_CONNECTION_NAME);
    }
    // シングルトンインスタンスはアプリケーション終了時にOSによってクリーンアップされる
}

// ヘルパー関数: テーブルの存在を確認し、なければ作成
void DatabaseManager::ensureTableExists(QSqlQuery& query, const QString& tableName, const QString& createSql)
{
    if (!m_database.tables().contains(tableName)) {
        if (!query.exec(createSql)) {
            qDebug() << "エラー: テーブル '" << tableName << "' の作成に失敗しました。" << query.lastError();
        } else {
            qDebug() << "テーブル '" << tableName << "' を作成しました。";
        }
    }
}

// ヘルパー関数: 雷管UIDを文字列に変換(仮の実装)
QString DatabaseManager::convertDetonatorIdToString(long long uid)
{
    // 実際のアプリケーションでは、UIDの具体的な形式に応じて変換ロジックを実装
    return QString::number(uid);
}

// 雷管パラメータを処理し、データベースに保存するメソッド
void DatabaseManager::processDetonatorParameters(DetonatorMessage* messages, int count)
{
    QSqlQuery dbQuery(m_database);

    for (int i = 0; i < count; ++i) {
        DetonatorMessage* currentMsg = messages + i;

        // 設計済みフラグが立っている場合は処理をスキップ
        if (currentMsg->Det_status & DesignFlag) {
            continue;
        }

        QString detonatorId = convertDetonatorIdToString(currentMsg->Uid);
        int delayTime = currentMsg->DT;
        int areaId = (currentMsg->KW >> 10);      // KWの上位ビットからエリアIDを抽出
        int holeId = (currentMsg->KW & 0x3FF);    // KWの下位ビットからホールIDを抽出
        int authStatus = (currentMsg->Det_status & AuthFlag) ? 1 : 0; // 認証済みなら1、そうでなければ0

        // 同じdetonator_codeを持つレコードが既に存在するか確認
        dbQuery.prepare("SELECT id FROM detonator_delay_settings WHERE detonator_code = ?");
        dbQuery.bindValue(0, detonatorId);
        if (!dbQuery.exec()) {
            qDebug() << "エラー: レコード存在チェックに失敗しました。" << dbQuery.lastError();
            continue; // 次のメッセージへ
        }

        if (!dbQuery.next()) { // レコードが存在しない場合のみ挿入
            dbQuery.prepare("INSERT INTO detonator_delay_settings (detonator_code, delay_time, area_id, hole_id, auth_status) "
                            "VALUES (?, ?, ?, ?, ?)");
            dbQuery.bindValue(0, detonatorId);
            dbQuery.bindValue(1, delayTime);
            dbQuery.bindValue(2, areaId);
            dbQuery.bindValue(3, holeId);
            dbQuery.bindValue(4, authStatus);

            if (!dbQuery.exec()) {
                qDebug() << "エラー: 遅延パラメータの挿入に失敗しました。" << dbQuery.lastError();
            } else {
                // 挿入成功後、メッセージオブジェクトの設計済み状態を更新
                currentMsg->DesignDT = currentMsg->DT;
                currentMsg->DesignKW = currentMsg->KW;
                currentMsg->Det_status |= DesignFlag; // 設計済みフラグを設定
                qDebug() << "新しい遅延パラメータが挿入されました: ID " << detonatorId;
            }
        } else {
            // レコードが既に存在する場合、ここでは何もしない(元のロジックに倣う)
            // 必要であれば、ここでUPDATE処理などを実装することも可能
            qDebug() << "遅延パラメータは既に存在します: ID " << detonatorId;
        }
    }
}

// メイン関数での使用例 (完全なMOC処理のためには適切なプロジェクト設定が必要)
/*
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // DatabaseManagerのインスタンスを取得
    DatabaseManager* dbManager = DatabaseManager::instance();
    QSqlDatabase currentDb = dbManager->getDatabase();

    // 例として雷管メッセージデータを作成
    DatabaseManager::DetonatorMessage messages[2];
    messages[0] = {1001, 100, (1 << 10) | 50, 0, 0, 0}; // Area 1, Hole 50, 未設計
    messages[1] = {1002, 120, (2 << 10) | 60, DatabaseManager::DesignFlag, 0, 0}; // Area 2, Hole 60, 設計済み

    // 雷管パラメータを処理
    dbManager->processDetonatorParameters(messages, 2);

    return a.exec();
}
*/

タグ: Qt SQLite SQL C++ QSqlDatabase

7月31日 16:52 投稿