Qtの基礎 - データベース

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

Qtでデータベース操作を行う場合、QtSqlモジュールが必要である。

QtSqlモジュールはデータベース操作のための機能を提供するQtのコンポーネントであり、
QSqlDatabase、QSqlQuery、QSqlTableModel等のクラスにアクセスして、データベース操作を効率的に行うことができる。

このモジュールは、SQLite、MySQL、PostgreSQL、Oracle、SQL Server等、多くのデータベースシステムをサポートしており、
使用するデータベースに応じて、適切なドライバーを選択する必要がある。


データベース操作の流れ

  1. データベース接続
    QSqlDatabaseクラスを使用して接続を確立する。
    ドライバの設定、ホスト名、データベース名、ユーザー名、パスワード等を指定する。

  2. クエリの実行
    QSqlQueryクラスを使用してSQLクエリを実行する。

  3. 結果の取得
    SELECTクエリの結果はQSqlQueryオブジェクトから取得することができる。
    nextメソッドを使用して、結果セットを反復処理する。

  4. トランザクション処理
    QSqlDatabaseクラスのtransactionメソッド、commitメソッド、rollbackメソッドを使用する。

  5. エラー処理
    QSqlErrorクラスを使用してエラー情報を取得して処理する。

  6. モデル/ビュー
    QSqlTableModelクラスやQSqlRelationalTableModelクラスを使用してデータをGUIに表示することも可能である。



MariaDB

以下の例では、MariaDBのデータベースを使用して、以下に示すようなテーブルを作成して基本的なCRUD操作を行っている。
実務では、必要に応じてフィールドを追加、または、データ型や制約を調整したりする必要がある。

  • テーブル名
    people
  • id
    INT型
    制約 1 : AUTO_INCREMENT
    制約 2 : PRIMARY KEY
    各レコードを一意に識別するための自動増分の主キー。
  • name
    VARCHAR(40)型
    制約 : NOT NULL
    名前を格納するフィールドで、最大40文字まで保存可能。
  • age
    INT型
    年齢を格納する整数型のフィールド。


また、使用時にはエラーハンドリングおよびセキュリティ対策 (SQLインジェクション対策等) を適切に実装することを推奨する。

  • ロギング機能の追加
  • リトライメカニズムの実装 (一時的なネットワーク問題などに対処するため)
  • 詳細なエラータイプの定義 (例:接続エラー、クエリエラー等)
  • パラメータのバリデーション
  • SQLインジェクション対策の強化


開発向けMariaDBライブラリをインストールする。

# RHEL
sudo dnf install mariadb-devel

# SUSE
sudo zypper install libmariadbd-devel


  • Qtプロジェクトファイルを使用する場合
 # Qtプロジェクトファイル (.pro)
 
 # pkg-configを使用してMariaDBの設定を取得
 CONFIG    += link_pkgconfig
 PKGCONFIG += libmariadb
 
 # 必要に応じて、追加のコンパイルフラグやリンクフラグを指定することもできる
 #QMAKE_CXXFLAGS += $$system(pkg-config --cflags libmariadb)
 #LIBS           += $$system(pkg-config --libs libmariadb)


  • CMakeLists.txtを使用する場合
 # CMakeLists.txtファイル
 
 # pkg-configを使用してMariaDBライブラリを検索
 find_package(PkgConfig REQUIRED)
 pkg_check_modules(MARIADB REQUIRED libmariadb)
 
 # QtライブラリとMariaDBライブラリをリンク
 target_link_libraries(<ターゲット名> PRIVATE 
    Qt${QT_VERSION_MAJOR}::Sql
    ${MARIADB_LIBRARIES}
 )
 
 # MariaDBのヘッダファイルのディレクトリを追加
 target_include_directories(<ターゲット名> PRIVATE
    ${MARIADB_INCLUDE_DIRS}
 )
 
 # MariaDBのコンパイルフラグを追加
 target_compile_options(<ターゲット名> PRIVATE
    ${MARIADB_CFLAGS_OTHER}
 )


 // DataBaseManager.hファイル
 
 #include <QtSql>
 #include <QString>
 #include <QSqlError>
 #include <stdexcept>
 #include <QDebug>
 
 class DatabaseException : public std::runtime_error
 {
 public:
    DatabaseException(const QString& message) : std::runtime_error(message.toStdString()) {}
 };
 
 class DatabaseManager
 {
 public:
    DatabaseManager();
    ~DatabaseManager();
 
    bool openConnection();
    void closeConnection();
    bool createTable();
    bool insertRecord(const QString &name, int age);
    bool updateRecord(int id, const QString &name, int age);
    bool deleteRecord(int id);
    QList<QMap<QString, QVariant>> readAllRecords();
 
    void beginTransaction();
    void commitTransaction();
    void rollbackTransaction();
 
    // 複数の操作をトランザクションで実行する場合のメソッド
    void performComplexOperation(const QString &name1, int age1, const QString &name2, int age2);
 
 private:
    QSqlDatabase db;
    void checkConnection();
    void handleDatabaseError(const QString &operation, const QSqlError &error);
 };


 // DataBaseManager.cppファイル
 
 #include "DataBaseManager.h"
 
 DatabaseManager::DatabaseManager()
 {
    db = QSqlDatabase::addDatabase("QMYSQL");
    db.setHostName("<ホスト名またはIPアドレス  例: localhost>");
    db.setDatabaseName("<データベース名  例: sampledb>");
    db.setUserName("<データベースのユーザ名>");
    db.setPassword("<データベースのユーザ名のパスワード>");
 }
 
 DatabaseManager::~DatabaseManager()
 {
    closeConnection();
 }
 
 // データベースの接続
 bool DatabaseManager::openConnection()
 {
    if (!db.open()) {
       throw DatabaseException("Failed to open database connection: " + db.lastError().text());
    }
 
    qDebug() << "Database connection opened successfully";
 }
 
 // データベースの切断
 void DatabaseManager::closeConnection()
 {
    if (db.isOpen()) {
       db.close();
       qDebug() << "Database connection closed";
    }
 }
 
 // 接続の確認
 void DatabaseManager::checkConnection()
 {
    if (!db.isOpen()) {
       throw DatabaseException("Database connection is not open");
    }
 }
 
 // エラーハンドリング処理
 void DatabaseManager::handleDatabaseError(const QString &operation, const QSqlError &error)
 {
    QString errorMessage = QString("%1 failed: %2").arg(operation, error.text());
    throw DatabaseException(errorMessage);
 }
 
 //  トランザクションを開始
 void DatabaseManager::beginTransaction()
 {
    checkConnection();
 
    if (!db.transaction()) {
       throw DatabaseException("Failed to start transaction: " + db.lastError().text());
    }
 
    qDebug() << "Transaction started";
 }
 
 // トランザクションをコミット
 void DatabaseManager::commitTransaction()
 {
    checkConnection();
 
    if (!db.commit()) {
       throw DatabaseException("Failed to commit transaction: " + db.lastError().text());
    }
 
    qDebug() << "Transaction committed";
 }
 
 // トランザクションをロールバック
 void DatabaseManager::rollbackTransaction()
 {
    checkConnection();
 
    if (!db.rollback()) {
       throw DatabaseException("Failed to rollback transaction: " + db.lastError().text());
    }
 
    qDebug() << "Transaction rolled back";
 }
 
 // 複数の操作を1つのトランザクションとして実行するメソッド
 void DatabaseManager::performComplexOperation(const QString &name1, int age1, const QString &name2, int age2)
 {
    try {
       beginTransaction();
 
       insertRecord(name1, age1);
       insertRecord(name2, age2);
 
       commitTransaction();
 
       qDebug() << "Complex operation completed successfully";
    }
    catch (const DatabaseException& e) {
       rollbackTransaction();
       throw DatabaseException("Complex operation failed: " + QString(e.what()));
    }
 }
 
 // テーブルの作成
 bool DatabaseManager::createTable()
 {
    checkConnection();
 
    QSqlQuery query;
    if (!query.exec("CREATE TABLE IF NOT EXISTS people (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(40) NOT NULL, age INT)")) {
       handleDatabaseError("Create table", query.lastError());
    }
 
    qDebug() << "Table 'people' created or already exists";
 }
 
 // 単一のレコードの追加
 bool DatabaseManager::insertRecord(const QString& name, int age)
 {
    checkConnection();
 
    QSqlQuery query;
    query.prepare("INSERT INTO people (name, age) VALUES (:name, :age)");
    query.bindValue(":name", name);
    query.bindValue(":age", age);
 
    if (!query.exec()) {
       handleDatabaseError("Insert record", query.lastError());
    }
 
    qDebug() << "Record inserted successfully";
 }
 
 // 単一のレコードの更新
 bool DatabaseManager::updateRecord(int id, const QString& name, int age)
 {
    checkConnection();
 
    QSqlQuery query;
    query.prepare("UPDATE people SET name = :name, age = :age WHERE id = :id");
    query.bindValue(":id", id);
    query.bindValue(":name", name);
    query.bindValue(":age", age);
 
    if (!query.exec()) {
       handleDatabaseError("Update record", query.lastError());
    }
 
    if (query.numRowsAffected() == 0) {
       throw DatabaseException(QString("No record found with id: %1").arg(id));
    }
 
    qDebug() << "Record updated successfully";
 }
 
 // レコードの削除
 bool DatabaseManager::deleteRecord(int id)
 {
    checkConnection();
 
    QSqlQuery query;
    query.prepare("DELETE FROM people WHERE id = :id");
    query.bindValue(":id", id);
    
    if (!query.exec()) {
       handleDatabaseError("Delete record", query.lastError());
    }
 
    if (query.numRowsAffected() == 0) {
       throw DatabaseException(QString("No record found with id: %1").arg(id));
    }
 
    qDebug() << "Record deleted successfully";
 }
 
 // 全てのレコードを読み込む
 QList<QMap<QString, QVariant>> DatabaseManager::readAllRecords()
 {
    checkConnection();
 
    QSqlQuery query("SELECT * FROM people");
    QList<QMap<QString, QVariant>> results;
    
    if (!query.exec()) {
       handleDatabaseError("Read all records", query.lastError());
    }
 
    while (query.next()) {
       QMap<QString, QVariant> record;
       record["id"] = query.value(0);
       record["name"] = query.value(1);
       record["age"] = query.value(2);
       results.append(record);
    }
 
    return results;
 }


 // main.cppファイル
 
 int main()
 {
    DatabaseManager dbManager;
 
    try {
       // データーベースの接続
       dbManager.openConnection();
       
       // テーブルの作成
       dbManager.createTable();
 
       // トランザクションを使用した複雑な操作
       dbManager.performComplexOperation("Alice", 28, "Bob", 32);
 
       // 個別のトランザクション管理の例
       dbManager.beginTransaction();
       try {
          // 既存のレコードの更新
          dbManager.updateRecord(1, "John Doe Updated", 31);
 
          // 任意のレコードの削除
          dbManager.deleteRecord(2);
          dbManager.commitTransaction();
       }
       catch (const DatabaseException& e) {
          dbManager.rollbackTransaction();
          qCritical() << "Transaction failed:" << e.what();
       }
 
       // 全てのレコードを読み込む
       QList<QMap<QString, QVariant>> records = dbManager.readAllRecords();
       for (const auto &record : records) {
          qDebug() << "ID:" << record["id"].toInt() 
                   << "Name:" << record["name"].toString() 
                   << "Age:" << record["age"].toInt();
       }
  
        // 全てのレコードを読み込む
        records = dbManager.readAllRecords();
        for (const auto& record : records) {
           qDebug() << "ID:" << record["id"].toInt() 
                    << "Name:" << record["name"].toString() 
                    << "Age:" << record["age"].toInt();
        }
    }
    catch (const DatabaseException &e) {
       // データベース関連のエラー
       qCritical() << "Database error: " << e.what();
    }
    catch (const std::exception &e) {
       // その他のエラー
       qCritical() << "Unexpected error: " << e.what();
    }
 
    return 0;
 }


トランザクションを使用するメリットを以下に示す。

  • データの一貫性
    複数の操作を1つの単位として扱うことができる。
  • エラー回復
    問題が発生した場合、簡単に全ての変更を元に戻すことができる。
  • パフォーマンス
    多数の操作を一度のトランザクションで行うことにより、データベースとの通信回数を減らし、パフォーマンスを向上させることができる。


ただし、トランザクション管理を使用する場合は、以下に示す事柄に注意すること。

  • トランザクションは必要な場合にのみ使用して、不必要に長く保持しないようにする。
  • ネストされたトランザクションの扱いには注意が必要である。
    QSQLDatabaseクラスは、一般的にネストされたトランザクションをサポートしていない。
  • 大規模なデータ操作を行う場合は、適切なチェックポイントを設定することを検討する。



SQLite3

Qtを使用してSQLite3のテーブルにアクセスする場合、まず、QSqlDatabaseクラスを使用してデータベースに接続する。

 #include <QSqlDatabase>
 #include <QSqlQuery>
 
 // SQLiteデータベースに接続する
 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
 db.setDatabaseName("<SQLiteデータベース名>.sqlite");  // データベースファイル名
 
 if (!db.open())
 {
    qDebug() << "データベースに接続できませんでした。";
    return;
 }


次に、QSqlQueryクラスを使用してクエリを実行する。
以下の例では、テーブルのデータを取得している。

 QSqlQuery query;
 query.prepare("SELECT * FROM <SQLiteデータベースのテーブル名>");
 
 if (query.exec())
 {
    while (query.next())
    {  // レコードの処理
       QString column1 = query.value(0).toString(); // 1番目の列
       QString column2 = query.value(1).toString(); // 2番目の列
 
       // 他の列も同様に取得できる
    }
 }
 else
 {
    qDebug() << "クエリの実行に失敗しました.";
 }
 
 // データベース接続を閉じる
 db.close();



SQLインジェクション対策

SQLインジェクション対策では、プレースホルダを使用してクエリを構築することが重要である。

以下の例では、":username"がプレースホルダとなり、ユーザからの入力等が直接クエリに組み込まれることなく、安全にクエリが実行される。
また、ユーザからの入力をクエリに組み込む場合、入力データを適切にサニタイズしてクエリに含めるようにすることにより、悪意のあるSQLコードが挿入されても実行されないようにする。

 QSqlQuery query;
 QString username = "user_input";  // ユーザからの入力等
 
 // プレースホルダを使用して安全にクエリを構築
 query.prepare("SELECT * FROM users WHERE username = :username");
 query.bindValue(":username", username);
 
 if (query.exec())
 {
    while (query.next())
    {  // レコードの処理
       // ...略
    }
 }
 else
 {
    qDebug() << "クエリの実行に失敗しました.";
 }