Taro3

View on GitHub

データをデータベースに保存する

データクラスの準備ができたので、データベース層の実装に進みます。Qtにはすぐに使えるsqlモジュールが用意されています。Qtでは、SQLデータベースドライバを使って様々なデータベースをサポートしています。gallery-desktopでは、sqlモジュールに含まれているSQLITE3ドライバを使用します。

データベースには複数の場所からアクセスすることになります。そのためには、単一のエントリ・ポイントが必要です。DatabaseManagerという名前の新しいC++クラスを作成し、DatabaseManager.hを以下のように変更します。

#include <QString>

class QSqlDatabase;

const QString DATABASE_FILENAME = "gallery.db";

class DatabaseManager
{
public:
    static DatabaseManager& instance();
    ~DatabaseManager();

protected:
    DatabaseManager(const QString& path = DATABASE_FILENAME);
    DatabaseManager& operator=(const DatabaseManager& rhs);

private:
    QSqlDatabase* mDatabase;
};

最初に注意すべき点は、第2章「QMakeの秘密を探る」の「SysInfoをシングルトンで変換する」で行ったように、DatabaseManagerクラスにシングルトンパターンを実装していることです。DatabaseManagerクラスは、mDatabaseフィールドで接続を開き、他の使用する可能性のあるクラスに貸し出します。

また、QSqlDatabaseは前方に宣言され、mDatabaseフィールドのポインタとして使用されます。 QSqlDatabaseヘッダを含めることもできましたが、望ましくない副作用が発生してしまいます: DatabaseManagerをインクルードするすべてのファイルに、QSqlDatabaseをインクルードしてしまいます。そのため、アプリケーション内に(gallery-coreライブラリにリンクする)他動的なインクルージョンがある場合、アプリケーションはSQLモジュールを有効にしなければなりません。結果として、ストレージレイヤはライブラリを介してリークします。アプリケーションはストレージレイヤの実装について何も知らないはずです。アプリケーションが気にすることは、SQLでもXMLでも何でもいいのです。ライブラリは、設計を尊重し、データを永続化すべきブラックボックスです。

DatabaseManager.cppに切り替えて、データベース接続を開いてみましょう。

#include "databasemanager.h"

#include <QSqlDatabase>

DatabaseManager &DatabaseManager::instance()
{
    static DatabaseManager singleton;
    return singleton;
}

DatabaseManager::~DatabaseManager()
{
    mDatabase->close();
    delete mDatabase;
}

DatabaseManager::DatabaseManager(const QString &path) :
    mDatabase(new QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE")))
{
    mDatabase->setDatabaseName(path);
    mDatabase->open();
}

QSqlDatabase::addDatabase(“QSQLITE”)関数呼び出しによるmDatabaseフィールドの初期化で正しいデータベースドライバが選択されます。以下の手順は、データベース名(ちなみにこれはSQLITE3のファイルパスです)を設定し、mDatabase->open()関数で接続を開くだけです。DatabaseManagerのデストラクタでは、接続が閉じられ、mDatabaseポインタが適切に削除されます。

データベースのリンクが開かれました。あとは、AlbumとPictureのクエリを実行するだけです。DatabaseManager の両方のデータクラスに CRUD (Create/Read/Update/Delete) を実装すると、DatabaseManager.cpp はあっという間に数百行になります。さらにいくつかのテーブルを追加すると、DatabaseManagerがどのような怪物になるか、もうおわかりでしょう。

このため、各データクラスには専用のデータベースクラスがあり、すべてのデータベース CRUD 操作を担当します。まず、Album クラスから始めます。新しい C++ クラス名 AlbumDao (データアクセスオブジェクト)を作成し、AlbumDao.h を更新します。

class QSqlDatabase;

class AlbumDao
{
public:
    AlbumDao(QSqlDatabase& database);
    void init() const;

private:
    QSqlDatabase& mDatabase;
};

AlbumDao クラスのコンストラクタは QSqlDatabase& パラメータを取ります。このパラメータは、 AlbumDao クラスが行うすべての SQL クエリに使用されるデータベース接続です。 init() 関数は albums テーブルを作成することを目的としており、 mDatabase が開かれたときに呼び出される必要があります。

AlbumDao.cppの実装を見てみましょう。

#include "albumdao.h"

#include <QSqlDatabase>
#include <QSqlQuery>
#include <QStringList>

#include "databasemanager.h"

AlbumDao::AlbumDao(QSqlDatabase &database) :
    mDatabase(database)
{
}

void AlbumDao::init() const
{
    if (!mDatabase.tables().contains("albums")) {
        QSqlQuery query(mDatabase);
        query.exec("CREATE TABLE albums (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)");
    }
}

いつものように、mDatabaseフィールドはデータベースパラメータで初期化されます。init()関数では、実際のSQLリクエストの動作を見ることができます。テーブルalbumsクラスが存在しない場合、mDatabase接続を実行するためのQSqlQueryクエリが作成されます。mDatabase を省略した場合、クエリはデフォルトの匿名接続を使用します。query.exec() 関数は、クエリを実行する最もシンプルな方法です。クエリのQString型を渡すだけで実行されます。ここでは、データクラス Album (id と name) にマッチするフィールドを持つ albums テーブルを作成します。

Tip

QSqlQuery::exec() 関数は、リクエストが成功したかどうかを示す bool 値を返します。実運用コードでは、常にこの値をチェックしてください。QSqlQuery::lastError() を使用して、さらにエラーを調べることができます。例は、DatabaseManager::debugQuery() の章のソースコードにあります。

AlbumDao クラスの骨格ができました。次のステップは、DatabaseManager クラスにリンクすることです。DatabaseManager クラスを次のように更新します。

// In DatabaseManager.h

#include "albumdao.h"

...

private:
    QSqlDatabase* mDatabase;

public:
    const AlbumDao albumDao;
};

// In DatabaseManager.cpp
DatabaseManager::DatabaseManager(const QString &path) :
    mDatabase(new QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE"))),
    albumDao(*mDatabase)
{
    mDatabase->setDatabaseName(path);
    mDatabase->open();

    albumDao.init();
}

albumDao フィールドは DatabaseManager.h ファイル内で public const AlbumDao として宣言されています。これには説明が必要です 。

DatabaseManagerのコンストラクタでは、albumDaoクラスは、mDatabase派生ポインタで初期化されます。albumDao.init() 関数は、データベース接続が開かれた後に呼び出されます。

私たちは今、より興味深い SQL クエリを実装するために進むことができます。私たちは、AlbumDaoクラスで新しいアルバムを作成することから始めることができます。

// In AlbumDao.h

class QSqlDatabase;
class Album;

class AlbumDao
{
public:
    AlbumDao(QSqlDatabase& database);
    void init() const;
    void addAlbum(Album& album) const;
    ...
};

// In AlbumDao.cpp

#include <QSqlDatabase>
#include <QSqlQuery>
#include <QStringList>
#include <QVariant>

...

void AlbumDao::addAlbum(Album &album) const
{
    QSqlQuery query(mDatabase);
    query.prepare("INSERT INTO albums (name) VALUES (:name)");
    query.bindValue(":name", album.name());
    query.exec();
    album.setId(query.lastInsertId().toInt());
}

関数 addAlbum() は album パラメータを受け取り、その情報を抽出して対応するクエリを実行します。ここでは、準備されたクエリの概念にアプローチします。 query.prepare() 関数は query パラメータを受け取ります。ここでは、nameパラメータに :name という構文を指定します。 二つの構文がサポートされています。コロン-名前を使用した Oracle スタイル (例 :name) と、クエスチョンマークを使用した ODBC スタイル (例 ?name) の 2 つです。

次に、:name 構文を album.name() 関数の値にバインドします。 QSqlQuery::bind()はパラメータ値としてQVariantを期待しているので、このクラスにincludeディレクティブを追加する必要があります。

一言で言えば、QVariantは、広範囲のプリミティブ型(char、int、doubleなど)や複雑な型(QString、QByteArray、QUrlなど)を受け付ける汎用データホルダーです。

query.exec() 関数が実行されると、バインドされた値が適切に置き換えられます。prepare()文のテクニックを使うと、SQLインジェクションに強くなり(隠されたリクエストを注入すると失敗します)、コードがより読みやすくなります。

クエリの実行は、オブジェクトクエリ自体の状態を変更します。QSqlQueryクエリは、単にSQLクエリの実行者ではなく、アクティブなクエリの状態も含んでいます。query.lastInsertId()関数を使用して、クエリに関する情報を取得することができます。この ID は、addAlbum() パラメータで指定した album に与えられます。私たちは album を変更するので、パラメータを const としてマークすることはできません。あなたのコードの const の正しさを厳密に守ることは、他の開発者にとって良いヒントになります。

残りの更新および削除操作は、addAlbum() で使用したのと厳密に同じパターンに従います。次のコードスニペットでは、期待される関数のシグネチャを提供するだけです。完全な実装については、この章のソースコードを参照してください。しかし、データベース内のすべてのアルバムを取得するリクエストを実装する必要があります。これはよく見てみる価値があります。

// In AlbumDao.h

#include <QVector>

    ...
    void addAlbum(Album& album) const;
    void updateAlbum(const Album& album) const;
    void removeAlbum(int id) const;
    QVector<Album*> albums() const;
    ...
};

// In AlbumDao.cpp

QVector<Album *> AlbumDao::albums() const
{
    QSqlQuery query("SELECT * FROM albums", mDatabase);
    query.exec();
    QVector<Album*> list;
    while (query.next()) {
        Album* album = new Album();
        album->setId(query.value("id").toInt());
        album->setName(query.value("name").toString());
        list.append(album);
    }
    return list;
}

albums()関数はQVector<Album>値を返さなければなりません。関数の本体を見てみると、QSqlQueryのもう一つのプロパティがあります。与えられたリクエストの複数の行をウォークスルーするために、queryは現在の行を指す内部カーソルを処理します。次に、new Album() 関数を作成し、 query.value() ステートメントで行データを埋めます。この新しい album パラメータが list に追加され、最後にこの list が呼び出し元に返されます。

PictureDaoクラスは、使い方も実装もAlbumDaoクラスに非常に似ています。主な違いは、ピクチャがアルバムへの外部キーを持っていることです。PictureDao関数は、albumIdパラメータによって条件付けされなければなりません。次のコードスニペットは、PictureDaoヘッダとinit()関数を示しています。

// In PictureDao.h

#include <QVector>

class QSqlDatabase;
class Picture;

class PictureDao
{
public:
    explicit PictureDao(QSqlDatabase& database);
    void init() const;

    void addPictureInAlbum(int albumId, Picture& picture) const;
    void removePicture(int id) const;
    void removePictureForAlbum(int albumId) const;
    QVector<Picture*> pictureForAlbum(int albumId) const;

private:
    QSqlDatabase& mDatabase;
};

// In PictureDao.cpp

void PictureDao::init() const
{
    if (!mDatabase.tables().contains("pictures")) {
        QSqlQuery query(mDatabase);
        query.exec(QString("CREATE TABLE pictures")
                   + " (id INTEGER PRIMARY KEY AUTOINCREMENT, "
                   + "album_id INTEGER, "
                   + "url TEXT");
    }
}

ご覧のように、複数の関数は、画像と所有するalbumパラメータの間のリンクを作るために、albumIdパラメータを取ります。init()関数では、外部キーはalbum_id INTEGER構文で表現されます。SQLITE3には適切な外部キー型がありません。これは非常にシンプルなデータベースであり、このタイプのフィールドには厳密な制約はありません。単純な整数が使用されます。

最後に、AlbumDaoで行ったのと同じように、DatabaseManagerクラスにPictureDao関数を追加します。多くの Dao クラスがある場合、DatabaseManager クラスに const Dao メンバを追加して init() 関数を呼び出すのは、すぐに面倒になるという議論もあるかもしれません。

解決策としては、純粋な仮想 init() 関数を持つ抽象的な Dao クラスを作ることが考えられます。DatabaseManager クラスは Dao レジストリを持ち、各 Dao を QHash<QString, const Dao> mDaos で QString キーにマッピングします。init() 関数呼び出しは for ループ内で呼び出され、QString キーを使用して Dao オブジェクトにアクセスされます。これはこのプロジェクトの範囲外ですが、興味深いアプローチです。


戻る