やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

QtSqlでCreateTableするにはSQL文を発行する方法しかない

 QSqlFieldを渡してテーブル作成するメソッドなどは無い。

現状のテーブル作成

 QtではSQL文を発行することでテーブルを作成している。というか、Qtではそこまでしかサポートされていない。

参考

QSqlDatabase _db = QSqlDatabase::addDatabase("QSQLITE", "Memo");
QString dbPath = QDir(QApplication::applicationDirPath()).filePath("Memo.sqlite3");
_db.setDatabaseName(dbPath);

QSqlDatabase db = QSqlDatabase::database("Memo");
QSqlQuery query(db);
query.exec(tr("create table Memo(id INTEGER PRIMARY KEY AUTOINCREMENT, Memo TEXT, Created TEXT)"));

 あとはDBの定義を変更できるらしい。既存のテーブルにのみ使える。

m_db = QSqlDatabase::addDatabase( "QSQLITE" );
m_db.setDatabaseName("testdb.sqlite");
m_db.open();
if(m_db.isOpen())
{
    m_images = new QSqlRelationalTableModel(this, m_db);
    m_images->setTable("images");
    m_images->setEditStrategy(QSqlTableModel::OnFieldChange);
    
    // here is where I would like to add columns like for example name(VARCHAR) and number(INT)
    // then I would like to "execute" this model to the database like "CREATE TABLE"
}

 だが、このDB変更は使うだろうか? DBの列を追加することが考えられるが、データはどうなるのだろうか。できることが少ない上に中途半端なので使い道がない気がする。

 Qtライブラリを使って自作のcreate tableライブラリを作れないか考えてみる。

ベースのみSQL文で作成

 ベースのみSQL文でcreate tableする。つまりid列のみ作成する。他の列はQSqlTableModelのメソッドを介して作成する。

QSqlDatabase _db = QSqlDatabase::addDatabase("QSQLITE", "SomeDb");
QString dbPath = QDir(QApplication::applicationDirPath()).filePath("SomeDb.sqlite3");
_db.setDatabaseName(dbPath);

QSqlDatabase db = QSqlDatabase::database("SomeDb");
QSqlQuery query(db);
query.exec(tr("create table SomeTable (id INTEGER PRIMARY KEY AUTOINCREMENT)"));

QSqlTableModel model(nullptr, db);
model.setTable("SomeDb");
model.setEditStrategy(QSqlTableModel::OnFieldChange);
model.select();

// 列の追加
QSqlField fMemo("Memo");
fMemo.setType(QMetaType::QString);
QSqlField fCreated("Created");
fMemo.setType(QMetaType::QDateTime);
model.record().append(fMemo);
model.record().append(fCreated);

db.close();

 この案の問題は、テーブル名を変更できないこと。これもSQL文の発行をする必要がある。

query.exec(tr("alter table SomeTable rename to NewTableName"));

 これをみると、alter table文でカラムを追加するときは以下の制限があるらしい。(フィールド=カラム=列)

  1. PRIMARY KEY や UNIQUE 制約は設定できない
  2. DEFAULT 制約を設定する時は、CURRENT_TIME/CURRENT_DATE/CURRENT_TIMESTAMPは指定できない
  3. NOT NULL 制約を設定する時は、NULL以外のデフォルト値の設定が必要

 これは厳しい。とくにUNIQUE制約が設定できないのはひどい。この案はカラムの追加によるテーブル作成なので、UNIQUE制約が設定できないことになる。実用性皆無だが、一旦良しとして考えを進める。

 インタフェースはたとえば次のようなものが考えられる。

TableCreator tc(db, "テーブル名"); // 一時テーブルの作成

// 列の追加
QSqlField fMemo("Memo");
fMemo.setType(QMetaType::QString);
QSqlField fCreated("Created");
fMemo.setType(QMetaType::QDateTime);
tc.record().append(fMemo);
tc.record().append(fCreated);

tc.setTableName("変更したテーブル名"); // alter table テーブル名 rename to 変更したテーブル名
tc.create(); // QSqlTableModel::record().append(QSqlField); おそらくQtライブラリ内部で次のSQL文を発行している `alter table 一時テーブル名 add column カラム名 型 制約;`
tc.toString(); // create tableのSQL文

 制約についてはQSqlFieldのメソッドで設定できると思う。要調査。

create table文作成ラッパクラス自作

 これは非常に面倒だと思われる。SQLマスターでもないかぎり実装すべき構文すらすべて把握するのは不可能。さらにRDBMSによっても構文が微妙に違ったりすると思う。それらの違いを調べるのだけでも面倒。SQLite3に限定しても面倒。

QSqlFieldからcreate tableの一部を作る

 SQL文の基本は以下。

create table SomeTable (列定義, ... 列定義)

 例外として外部キー制約の定義がある。他にもあるかも知れない。

create table SomeTable (
  列定義,
  ...
  列定義,
  FOREIGN KEY(列名) REFERENCES 外部テーブル名(列名)
)

 なお、外部キー定義は列定義に記述する記法もある。

create table ChildTable (ParentId REFERENCES Parent(Id)); 

 だが、QSqlFieldには外部キー設定を保持するようなメンバが存在しない。

 なので、外部キー定義のSQL文に関してはQSqlFieldを頼りにすることはできない。自作する必要がある。

 なお、外部キー制約を定義したあとのライブラリならあるようだ。結合テーブルを作成するQSqlRelationalTableModelクラスというのがある。子テーブルの値を参照できるらしい。もっとも、テーブル定義の段階では関係ないが。

 列の定義は以下のような構文。

列名 型 制約

 スペースで区切っている。だが、そう単純でもない。たとえばPRIMARY KEYなどはそれがひとつの要素なのにスペースが間に入っている。

id INTEGER PRIMARY KEY AUTOINCREMENT

 この構文を読み解くコードを書くのは面倒そう。もっとも、そんなコードを書く必要はない。ここではSQL文を書き出しさえすればいい。

諦めてSQL文で書く

 自作ライブラリなど無謀。以下のような問題が起こることはやる前にわかる。

  • 実装が超面倒そう(RDBMSごとにおけるSQL構文の調査)
  • 不正確なコードを書いてバグを作り込みそう
  • 保守できなさそう(RDBMSの更新を反映せねばならない)

せめてC++ソースコードから分離できないか

 以下のようなディレクトリを用意し、自動で読み込んでcreate table文を発行する。

  • Databasies/
    • SomeDatabase/
      • SomeTable.sql
SqlTableCreator creator;
creator.Create();
  • addDatabase(), removeDatabase(), database()するときの名前: SomeDatabase
  • SQLite3ファイルパス: SomeDatabase.sqlite3
    • ディレクトリパス: SqlTableCreator.SetDirPath(QString dirPath="./");
    • ファイル名、拡張子: SqlTableCreator.SetFileExtension(QString fileExtension="sqlite3");

もうスクリプトで良くない?

 sqlファイルに沿ってDB作成するだけならsqlite3コマンドだけでできるはず。だったらbashなどのスクリプトで十分ではないか。それをQtアプリで呼び出せばいい。

  • Qtのsqlite3とシステムコマンドのsqlite3は同じ? バージョン差異ない?: たぶんQtはシステムのsqlite3コマンドを参照していると思われる
  • OS環境による差異ない?: C++をビルドする時点でOS固有アプリになってしまうので心配するだけ無駄
  • テーブル定義がC++コードから消えるので列名がわからなくなってしまう: QSqlTableModel::record().field()で参照できるはず

 必要な情報があっちこっちに散らばってしまう。

まとめ

方法 問題
コード内にDDLを含める create tableなど生のSQLで書かねば実装できない。C++言語と混在してコードが保守しづらい
QSqlTableModel::record().append(QSqlField)でテーブル定義を変更する alter table文はUNIQUE制約が設定できない等の制限がある。QSqlFieldは外部キー制約の設定ができない
create table文作成ラッパクラス自作 実装や保守が大変すぎる(RDBMSごとにおけるSQL構文の調査、RDBMSの更新反映、不正確なコードを書いてバグを作り込みそう)
sqlファイル自動実行ライブラリ自作 実装は現実的な範囲と思われるが、C++で実装するメリットが微妙
sqlファイル自動実行bashスクリプト自作 C++に加え、shellスクリプト言語の技能が必要

 まとまってないな。

所感

 C#のEntityFramework(コードファースト)が使えたら良かったのに。

 現実的なのは、外部SQLファイルを読み込んでデータベースファイルを作成することか。これはSQLite3限定になりそう。Qtライブラリにするとさらに限定的になってしまうので、bashスクリプトがいいか?