2015年8月29日土曜日

[haskell][persistent][sqlite] Persistentパッケージのmigration機能のまとめ

HaskellでDB操作ができるPersistentパッケージの紹介をしましたが、このエントリではPersistentパッケージが提供しているmigration機能をまとめておきます。
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"})
      
  • NON NULL制約のカラム追加:NG
    • コード変更点
    • 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.)
      
  • カラムの型の変更:OK
    • コード変更点
    • 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の機能が足りないためだと思われます。
  1. 古いスキーマでperson_backupテーブルを生成
  2. person_backupテーブルにオリジナルのpersonテーブルの内容をコピー
  3. personテーブルを破棄
  4. 新しいスキーマでpersonテーブルを生成
  5. person_backupの内容をpersonにコピー
  6. person_backupテーブルを破棄

宿題:

自分の用途では、NON NULL制約のカラムを追加し、既存レコードの値にはデフォルト値を埋めた状態にしたかったのですがPersistent備え付けの機能ではできないようです。
migration処理はTemplateHaskellによって生成されたコードで実行されているらしいので、そのあたりを弄ればなんとかなるのかも。
カラム名の変更も新スキーマのカラムと旧スキーマのカラム名の対応をmigration処理にうまく伝えることができれば、なんとかなりそうな気がします。このあたりは今後の課題として、方法がわかったらまたblogにまとめます。

参考:

2015年8月17日月曜日

[cygwin][emacs] cygwin版emacsでgtagsを使えるようにする方法

これまでタグジャンプにはctagsを用いていたのですが、gtagsの「呼び出し元も一覧できる」という機能に惹かれてセットアップしてみました。いくつかはまりポイントがあったのでその対応方法も含め、手順を残しておきます。
cgwin提供のパッケージにはgtagsは含まれていなかったため、自前でビルドすることにしました。

目次:

  • gatgsの特徴
  • gtagsのインストール手順
  • emacsの設定
  • gtagsの使い方


gtagsの特徴:

  • 良い点!
    • 関数の呼び出し元を一覧表示しジャンプできる(gtags-find-rtag)
    • 関数だけでなく、ローカル変数を含む任意のシンボルをタグジャンプできる(gtags-find-symbol)
      • 正規表現で一覧絞り込み可能(gtags-find-pattern)
      • 開いているファイルで絞り込み可能(gtags-parsefiile)
    • 特定文字列をファイル名に含むファイルを一覧できる(gtags-find-file)
    • ジャンプした場所をスタックで記憶しており、何個でも遡れることができる(gtags-pop-stack)
      • コールスタックを調査している場合、この機能が非常に役に立ちます。
  • 悪い点…orz
    • ctags(find-tag)ではclass宣言にジャンプできましたが、gtags-find-tagではタグジャンプできませんでした(typedef, struct, enum定義はOKなのですが)…。コンストラクタ・デストラクタしかタグづけされておらず、class宣言の場所はgtags-find-rtagで探し出さないといけない状態です。
      • できる!というかた是非手順をご教示くださいm(_ _)m


gtagsインストール手順:

  1. ローカルで適当なディレクトリを作って、GNU GLOBAL source code tagging systemからソースコードのglobal-6.5.tar.gzアーカイブをダウンロードします。
    • (参考)DOS and Windows portsport版のglo65s.zipもありますが、cygwin環境でコンパイル&利用する上ではport版を利用する必要はないようです。port版をビルドする際には、以下の手順でconfigureにchmodで実行可能属性を付与する必要があります。
      • % chmod +x configure
        
      • zipを展開しただけだと実行権限がついておらず、configureスクリプトを実行しようとすると以下のようなエラーになります。
        % ./configure
        ./configure: Permission denied.
        
  2. zipアーカイブを展開し、生成されたglobal-6.5ディレクトリに移動
    • % tar xzf global-6.5.tar.gz
      % cd global-6.5
      
    • 全てのファイルがglobal-6.5ディレクトリ以下に展開されます。
  3. ./configureを実行
    • % ./configure
      
    • 足りないライブラリがあればcygwinパッケージでインストールする。自分の環境では以下のライブラリの追加インストールが必要でした。
      • libncurses-devel
    • このパッケージがない状態で./configureを実行すると以下のようなエラーになります。
    • % ./configure
       (...snip...)
      configure: checking "location of ncurses.h file"...
      configure: error: curses library is required but not found.
      If you are not going to use gtags-cscope, please try ./configure --disable-gtagscscope
      
  4. makeと、make installを実行
    • % make
      % make install
      
    • デフォルトでは/usr/local以下にファイルがコピーされます。
  5. 念のため確認
    • % rehash
      % which gtags
      /usr/local/bin/gtags
      % gtags --version
      gtags (GNU GLOBAL) 6.5
      Copyright (c) 2015 Tama Communications Corporation
      License GPLv3+: GNU GPL version 3 or later <http://www.gnu.org/licenses/gpl.html>
      This is free software; you are free to change and redistribute it.
      There is NO WARRANTY, to the extent permitted by law.
      
    • /usr/local/binにPATHが通っていないと以下のようなエラーになります。
      % which gtags
      gtags: Command not found.
      


emacsの設定:

  1. gtags.elをload-pathに移動する
    • % cd /usr/share/emacs/site-lisp
      % ln -s /usr/local/share/gtags/gtags.el . 
      
    • load-pathに/usr/local/share/gtagsを追加する方法でも問題はありません。個人的な好みでsymbolic linkを利用しています。
  2. .emacsの編集
    • ;;; gtags ;;;
      (require 'gtags)
      (add-hook 'java-mode-hook (lambda () (gtags-mode 1)))
      (add-hook 'c-mode-hook (lambda () (gtags-mode 1)))
      (add-hook 'c++-mode-hook (lambda () (gtags-mode 1)))
      
  3. キーバインドのカスタマイズ
    • (setq gtags-mode-hook
            '(lambda ()
               (local-set-key "\C-j\C-t" 'gtags-find-tag)
               (local-set-key "\C-j\C-h" 'gtags-find-tag-from-here)
               (local-set-key "\C-j\C-p" 'gtags-find-pattern)
               (local-set-key "\C-j\C-r" 'gtags-find-rtag)
               (local-set-key "\C-j\C-s" 'gtags-find-symbol)
               (local-set-key "\C-j\C-f" 'gtags-find-file)
               (local-set-key "\C-j\C-l" 'gtags-parse-file)
               (local-set-key "\C-j\C-j" 'gtags-pop-stack)
               ))
      (setq gtags-select-mode-hook
            '(lambda ()
               (local-set-key "\C-j\C-j" 'gtags-pop-stack)
        (local-set-key [127] 'gtags-pop-stack)      ; [DEL]
               ))
      
    • M-t, M-r, M-s, C-tにバインドする例が紹介されていましたが、個人的に利用するものとバッティングしていため、C-j C-???にマッピングしています。
    • C-tでは不要でしたが、C-j C-jをgtags-pop-stackにバインドするには、gtags-select-mode-hookでの定義も必要でした。


gtagsの使い方:

  1. ソース群が格納されたディレクトリのルートへ移動してGTAGSファイルを生成する
  2. % cd src
    % gtags -v
    
  3. emacsを起動しgtags-find-tagで関数名を指定し[RET]
    • 初回はGTAGSファイルの場所を聞かれます。gtagsを実行したディレクトリに生成されているファイルを選択します。

備考:

cygwin以外の環境(mac, linux)でも同様の手順でセットアップができました。
cygwin-mountを入れないと動かないかも、という記述を見かけましたが私の(以下の)環境では問題ありませんでした。
% uname -a
CYGWIN_NT-6.3 win8 2.2.0(0.289/5/3) 2015-08-03 12:51 x86_64 Cygwin


参考: