Alloy Modelの学習・その2(マイグレーション)
前回の学習内容をもとに、せっかくなのでマイグレーション機能も試してみることにしました。
今回のサンプルは前回と同じプロジェクトの step2
というブランチにあります。
umi-uyura/AlloyModelStudy at step2
マイグレーションを試す場合は、まず master
ブランチをビルドして動かしてから、 step2
ブランチをビルドして実行することで発動します。
マイグレーションする内容
マイグレーションとは、テーブルにカラムを追加したいといった場合に、データベースを作りなおすのではなく、データを保持したまま変更を加える方法。
というわけで、前回のプロジェクトで作成したテーブルを加工してみることにしました。
ただ、新しい項目を加えるとUIも変更しなくてはならなくなり少々面倒なので、既存データの扱いを変更することで試すことに。
前回時のデータベースの構造は以下のとおり。
内容 | カラム | 型 | 主キー | 他 |
---|---|---|---|---|
レコードの識別子 | id | INTEGER | ◯ | AUTOINCREMENT |
メモの内容 | contents | TEXT | - | - |
変更内容としては、お気に入りということで文字列の先頭につけていた *
を分離する、というものにしました。また *
の登録
できる数には制限をかけていなかったこともあり、その数が多いほど優先度が高い扱いにすることにしました。
そこで、マイグレーション後のテーブル構造は以下のように変更することにしました。
内容 | カラム | 型 | 主キー | 他 |
---|---|---|---|---|
レコードの識別子 | id | INTEGER | ◯ | AUTOINCREMENT |
メモの内容 | contents | TEXT | - | - |
メモの優先度 | priority | TEXT | - | - |
よってマイグレーションで実施する処理としては、 contents
から先頭の *
部分を切り出して、 priority
へセットする、というものになります。
マイグレーション処理の実装
マイグレーションファイルの生成
マイグレーションの内容はJavaScriptで実装できます。
そのファイル名は、モデル名をベースに、マイグレーションの世代管理のための日時情報を YYYYMMDDHHmmss_
として接頭部に付与したファイルを用意します。
この雛形は、 alloy generate
コマンドで生成することもできます。
$ alloy generate migration memo [INFO] Generated migration named 201602180643382_memo
上記のコマンドを実行した例としては、 app/migrations
フォルダ内に 201602180643382_memo.js が生成されます。
マイグレーションファイルの構造
生成されたマイグレーションファイルは、以下の2つの関数によって構成されています。
migration.up = function(migrator) { }; migration.down = function(migrator) { };
どちらも migrator
というオブジェクトを引数に取っていますが、この中にデータベースオブジェクトの参照や、テーブル名などの情報、そしてデータベース操作用の関数を持っています。この migrator
オブジェクトを使って、マイグレーション処理を実装します。
migrator
が持っている機能は以下のとおり。
キー | 説明 |
---|---|
db | Ti.Databaseインスタンス ※このインスタンスを閉じたり、別のDBを開いたりするのはNG! |
dbname | データベース名 |
table | テーブル名。 config.adapter.collection_name と同じもの。 |
idAttribute | データベースのプライマリーキーとなる属性名 |
createTable | データベースにテーブルを作成する。Alloy Modelの columns と同じ構造でテーブル定義を渡す。 |
dropTable | データベースからテーブルを削除する |
insertRow | テーブルにレコードを追加する |
deleteRow | テーブルからレコードを削除する |
migration.up()
は前回のバージョンからデータベースをアップグレードする処理を、 migration.down()
は変更点をロールバックして前回のバージョンに戻す処理を実装します。
ちなみに、そもそもですが、マイグレーション機能はsql sync adapterでのみ利用することができる機能のようです。
例)新しいテーブルを追加するマイグレーション
今回は既存のテーブルに変更を加えるパターンですが、新しいテーブルを追加する場合の例です。
この場合は、 migrator.createTable()
と migrator.dropTable()
を使ってマイグレーションおよびロールバック処理を書くようです。
Appceleratorドキュメントの例によると、以下のような感じです。
var preload_data = [ {title: 'To Kill a Mockingbird', author:'Harper Lee'}, {title: 'The Catcher in the Rye', author:'J. D. Salinger'}, {title: 'Of Mice and Men', author:'John Steinbeck'}, {title: 'Lord of the Flies', author:'William Golding'}, {title: 'The Great Gatsby', author:'F. Scott Fitzgerald'}, {title: 'Animal Farm', author:'George Orwell'} ]; migration.up = function(migrator) { migrator.createTable({ "columns": { "book": "TEXT", "author": "TEXT" } }); for (var i = 0; i < preload_data.length; i++) { migrator.insertRow(preload_data[i]); } }; migration.down = function(migrator) { migrator.dropTable(); };
migrator.createTable()
に、モデルの columns
と同じようにテーブル定義情報を渡すことで新しいテーブルが作成されるので、必要に応じてさらに新しいデータを migrator.insertRow()
で突っ込んでいます。
ロールバックする場合は、そのテーブル自体を migrator.dropTable()
で削除してしまえば、もとに戻せるということのようです。
カラムを追加するマイグレーション
今回のようなカラムを追加するようなマイグレーションの場合は、直接SQLを発行して処理します。
今回の場合、
- 優先度カラムを追加
- 既存データから優先度を判定してデータを更新
という処理が必要になるので、以下のような感じに実装してみました。
app/migrations/201602180643382_memo.js
migration.up = function(migrator) { var db = migrator.db; var table = migrator.table; migrator.createTable({ columns: { 'id': 'integer primary key autoincrement', 'contents': 'text' } }); db.execute('ALTER TABLE ' + table + ' ADD COLUMN priority STRING DEFAULT ""'); var rows = db.execute('SELECT * FROM ' + table + ';'); while (rows.isValidRow()) { var contents = rows.fieldByName('contents'); var priorityPos = contents.lastIndexOf('*'); if (0 > priorityPos) { rows.next(); continue; } var priorityString = contents.substring(0, priorityPos + 1); var contentsBody = contents.substring(priorityPos + 1); var updateQuery = 'UPDATE ' + table + ' SET priority = "' + priorityString + '", contents = "' + contentsBody + '" WHERE id = ' + rows.fieldByName('id') + ';'; db.execute(updateQuery); rows.next(); } };
SQLiteは DROP COLUMN
が使えないので、直接テーブルからカラムを削除することができません。そこで、カラムが減った新しいテーブルを作成して、そこにデータを入れなおす必要があるようです。
最初に migrator.createTable()
をしていますが、これはこのマイグレーションを実施するバージョンのアプリを新規でインストールする場合、まだテーブルが作成されていない可能性があるからです。 migrator.createTable()
は内部で IF NOT EXISTS
を使っているので、すでにテーブルがある状態で呼び出しても問題ありません。
その後、 ALTER TABLE
で priority
カラムを追加してから、 contents
の先頭にある *
を取り出して priority
へ格納するという処理を、すべての行に対して実施しています。
今回は関係ありませんでしたが、マイグレーション時の注意事項として、もしモデルに idAttribute
を指定していない場合、 alloy_id
というカラムを自動的に追加しているらしので、これを含めるのを忘れないようにする必要があるようです。
contents
から *
を取り除いたので、画面側にも priority
を表示するため、ListItemの表示内容にも priority
を追加しました。
app/views/index.jade
ListItem(title="{priority} {contents}", itemId="{id}")/
ロールバックする( priority
カラムがない状態に戻す)
さて、マイグレーションが想定通り動くようになったので、次に migration.down()
にロールバック処理を実装してみることに。
これは、データベースをマイグレーション前に戻す処理になります。
そこで、テンポラリのテーブルを作って、既存のデータを避難させ、改めてマイグレーション前の構造のテーブルを作成し、そこにデータを加工しつつ投入する、という感じで実装してみました。
migration.down = function(migrator) { var db = migrator.db; var table = migrator.table; var backup_table = table + '_backup'; db.execute('CREATE TEMPORARY TABLE ' + backup_table + '(id,contents,priority);'); db.execute('INSERT INTO ' + backup_table + ' SELECT id,contents,priority FROM ' + table + ';'); migrator.dropTable(); migrator.createTable({ columns: { 'id': 'integer primary key autoincrement', 'contents': 'text' } }); var rows = db.execute('SELECT * FROM ' + backup_table + ';'); while (rows.isValidRow()) { var id = rows.fieldByName('id'); var contents = rows.fieldByName('contents'); var priority = rows.fieldByName('priority'); var updateQuery = 'INSERT INTO ' + table + ' (id,contents) VALUES (' + id + ',"' + priority + contents + '");'; db.execute(updateQuery); rows.next(); } db.execute('DROP TABLE ' + backup_table + ';'); };
で、ここまで実装して、ふと気付いたのですが、
・・・ロールバックってどうやって確認するんだろ?
Alloy Modelのマイグレーション処理で、migration.down()ってどういうときに呼ばれるものなのでしょうか? #titaniumjp
— Umi Uyura (@umi_uyura) February 19, 2016
声(ツイート)に出てた。
Appceleratorのドキュメントにもそのあたり発見できず、いろいろ探していたところ、最近Q&AフォーラムがAppcelerator公式サイトから移行したStack Overflowの方で、それらしい回答を発見しました。
appcelerator - Lifecycle of Alloy Migrations - Stack Overflow
それによると、 migration.down()
は何らかの方法で古いバージョンのアプリへアップグレード?したときに発動するもよう。
ただし、App Storeで公開しているものでは発動しないみたいで、これはアプリをダウングレードする手段が提供されていないから、ということかもしれません。
よって使える場面としては、テスト中やAd Hoc/Enterprise配布のときということになりそうです。
確かに本番リリースしたアプリでロールバックさせるような状況って思いつかない(必要なら再度アプリをアップデートする)ので、あまり使う機会ないのかも。
ついでに表示順序も改良
どうせ優先度をつけるなら、ListViewへの表示順序も優先度が高いとしているものから並ばせたい、と思ったので、その部分も合わせて変更することにしました。
Backbone.Collectionは、基本的にモデルを追加した順序が基準になるようです。
そこで、 priority
の数が多い順が基準となるように、 Collection.comparator
を実装しました。
app/models/memo.js
exports.definition = { ... extendCollection: function(Collection) { comparator: function(data) { return (0 - data.get('priority').length); } }); return Collection; } };
これで、 model.fetch()
してきたときに priority
順で表示されるようになりました。
もう1点、お気に入りの登録をして priority
が変化した時には、 Collection.sort()
を呼び出して再度並び替えを実施することで、更新後のお気に入り数に応じた順序で表示されるようになりました。
app/controllers/index.js
function clickItem(e) { var optionDialog = Ti.UI.createOptionDialog(opt); optionDialog.addEventListener('click', function(e) { switch (e.index) { case 0: var m = memos.where({id: itemId}); if (m.length) { var priority = '*' + m[0].get('priority'); m[0].set({priority: priority}); m[0].save(); memos.sort(); // <= 追加 } break; } }); optionDialog.show(); }
Data Bindingのおかげで、UI部分を操作するコードは書くことなく、Collectionの順序が画面に反映されました。
見た目的には前回と変わっていないのですが、お気に入り( *
)の数が多いものが、より上位に表示されるようになりました。また、お気に入りの数を増やした場合、より上位に移動します。
感想
今のところマイグレーションを使うような予定はないのですが、そういうのもあるのか、ということで。
参考
- Alloy Sync Adapters and Migrations
- appcelerator - Lifecycle of Alloy Migrations - Stack Overflow
- TitaniumでMigrationを行う - Qiita
- Alloyでのmigrationについて調べてみた - Give it a shot
Appcelerator Titanium Smartphone App Development Cookbook - Second Edition
- 作者: Jason Kneen
- 出版社/メーカー: Packt Publishing
- 発売日: 2015/11/30
- メディア: Kindle版
- この商品を含むブログを見る