DBを作って運用していると、機能追加や仕様変更に伴いスキーマ変更が必要になるケースが多々あります。このようなケースにおいてPersistentのmigration機能がどれくらい使えるのかを調べた結果です。
基本:
Persistetのmigration機構は(保守的なルールに沿って)スキーマ変更をある程度まで自動で処理してくれます。ロードしたDB内のテーブル情報と、コードで定義されたEntity Definition(テーブル定義)を比較し、以下のケースにおいてスキーマの変更を行います。
- カラムの型を変更した場合:
- ただし、値の変換ができない場合には、DBによって拒否されることになります。
- カラムを追加した場合:
- ただし、追加したカラムにNOT NULL制約がある場合は、デフォルト値は設定されず、エラーとなります。
- カラムのNOT NULL制約を外してNULL許容にした場合:
- 変換を実行します。
- 逆にNULL許容カラムにNON NULL制約に変更するケースの結果は、DBの状態に依存します(NULL値が存在しなければ成功、存在していると失敗)。
- 新規Entity(テーブル)が追加された場合
Persistentは以下のケースには対応していません。
- フィールド、Entith(テーブル)のリネーム
- 変更前の名前と変更後の名前の対応関係がわからないので…。
- フィールドの削除
- データ喪失につながるので、デフォルトではフィールド削除はエラーとなります。ですが、runMigration()の代わりにrunMigrationUnsafe()を呼ぶことで強制的に削除することは可能です(もちろん非推奨)。
実験:
Migration対象のDBを以下のコードでold.dbという名前で生成します。このコードで生成されたold.dbを、新しいスキーマにmigrateすると、結果がどのようになるかをまとめておきます。{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Database.Persist import Database.Persist.TH import Database.Persist.Sqlite import Control.Monad.IO.Class (liftIO) share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person name String age Int deriving Show |] main :: IO () main = runSqlite "old.db" $ do -- this line added: that's it! runMigration migrateAll michaelId <- insert $ Person "Michael" 26 michael <- get michaelId liftIO $ print michael
以下、いくつかのスキーマ変更を実際に試してみた結果です。
- NULL許容カラムの追加:OK
- コードの変更点
Person name String age Int address String Maybe -- add new NULL-able column deriving Show |]
michaelId <- insert $ Person "Michael" 26 (Just "Tokyo")
Migrating: CREATE TEMP TABLE "person_backup"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" INTEGER NOT NULL,"address" VARCHAR NULL) Migrating: INSERT INTO "person_backup"("id","name","age") SELECT "id","name","age" FROM "person" Migrating: DROP TABLE "person" Migrating: CREATE TABLE "person"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" INTEGER NOT NULL,"address" VARCHAR NULL) Migrating: INSERT INTO "person" SELECT "id","name","age","address" FROM "person_backup" Migrating: DROP TABLE "person_backup" Just (Person {personName = "Michael", personAge = 26, personAddress = Just "Tokyo"})
- コード変更点
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person name String age Int address String -- add new NON NULL column deriving Show |]
michaelId <- insert $ Person "Michael" 26 "Tokyo"
Migrating: CREATE TEMP TABLE "person_backup"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" INTEGER NOT NULL,"address" VARCHAR NOT NULL) Migrating: INSERT INTO "person_backup"("id","name","age") SELECT "id","name","age" FROM "person" add_column: user error (SQLite3 returned ErrorConstraint while attempting to perform step.)
- コード変更点
Person name String age String -- change type from Int to String deriving Show |]
michaelId <- insert $ Person "Michael" "26"
Migrating: CREATE TEMP TABLE "person_backup"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" VARCHAR NOT NULL) Migrating: INSERT INTO "person_backup"("id","name","age") SELECT "id","name","age" FROM "person" Migrating: DROP TABLE "person" Migrating: CREATE TABLE "person"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" VARCHAR NOT NULL) Migrating: INSERT INTO "person" SELECT "id","name","age" FROM "person_backup" Migrating: DROP TABLE "person_backup" Just (Person {personName = "Michael", personAge = "26"})ログを見る限り以下の手順でmigrationを行っています。参照したYesodサイトに記載がありましたがSQLiteではALTER TABLEの機能が足りないためだと思われます。
- 古いスキーマでperson_backupテーブルを生成
- person_backupテーブルにオリジナルのpersonテーブルの内容をコピー
- personテーブルを破棄
- 新しいスキーマでpersonテーブルを生成
- person_backupの内容をpersonにコピー
- person_backupテーブルを破棄
宿題:
自分の用途では、NON NULL制約のカラムを追加し、既存レコードの値にはデフォルト値を埋めた状態にしたかったのですがPersistent備え付けの機能ではできないようです。migration処理はTemplateHaskellによって生成されたコードで実行されているらしいので、そのあたりを弄ればなんとかなるのかも。
カラム名の変更も新スキーマのカラムと旧スキーマのカラム名の対応をmigration処理にうまく伝えることができれば、なんとかなりそうな気がします。このあたりは今後の課題として、方法がわかったらまたblogにまとめます。
0 件のコメント:
コメントを投稿