MySQL のバックアップと世代管理

要件

  • バックアップはmysqldump
  • バックアップファイルは1週間前のまで残して欲しい
    • 8日以上前のものは消す


簡単じゃんと思ったら案外罠があった。

設定

バックアップ
MAILTO="youraddress"
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/home/admin/bin

0 4 * * * mysqldump -u root -p rootpass --default-character-set=utf8 --hex-blob --single-transaction --master-data=2 dbname | gzip > /var/backup/mysql/dbname_`date +"\%Y\%m\%d_\%H"`.tar.gz


crontab において「%」はメタ文字なのでエスケープする。エスケープしていないと、次のエラーに遭遇する。

/bin/sh: -c: line 0: unexpected EOF while looking for matching ``'
/bin/sh: -c: line 1: syntax error: unexpected end of file

「第 6」フィールド (行の残りの部分) には実行されるコマンドを指定する。その行のコマンド部 (改行文字または % 文字まで) が /bin/sh (またはその crontab ファイルの SHELL 環境変数で指定されたシェル) によって実行される。コマンド中にパーセント記号 (%) がバックスラッシュ (\) によってエスケープされずに置かれていると、改行文字に置き換えられ、最初に現れた % 以降の全てのデータは標準入力としてコマンドに送られる。

Manpage of CRONTAB
世代管理
0 3 * * * find /var/backup/mysql/ -type f -name "dbname*.tar.gz" -mtime +7 -daystart | xargs rm


8日以上前のを消す場合は「+8」ではなく「+7」になる。また、世代管理のjob とバックアップのjob が実行される時間の前後関係が逆になっても8日以上前のバックアップファイルを消せるように「daystart」オプションを使う。


find のオプションはややこしい。「+mtime +1」としても、昨日修正されたファイルは検索対象にならない。このことは、英語のman に書かれている(日本語のmanには書かれていない)。

  • atime n

File was last accessed n*24 hours ago. When find figures out how many 24-hour periods ago the file was last accessed, any
fractional part is ignored, so to match -atime +1, a file has to have been accessed at least two days ago.


要は、何時間前という時間単位でみているのではなく、あくまで24-hour という単位でみている。そして、+n はn 以上ではなく、n より上を表す。また、少数部分は切り捨てる。
以上より、「+mtime +1」は1 24-hour より前となるので、36時間前(1.5 24-hour 前)に修正されたファイルは対象にならない。

n 24-hour よりも前ではなく、n 時間以上前に修正されたという観点で考えたい場合は、「-mtime +n」は(n + 1) * 24 時間以上前と考える。

分かりにくいので、図に書いておく。

X でfind を実行した時、F で修正したファイルが検出されるのは「-mtime 2」か「-mtime +1」か「-mtime +0」。

    1/1     1/2     1/3     1/4     1/5
... -|-------|--F----|-------|---X---|- ...
  -- 3 --|-- 2 --|-- 1 --|-- 0 --|          # n 24-hour ago
  - +2 --|-- 2 --|----- -2 ------|          # -n, n, +n の考え方

  ---|-- 3 --|-- 2 --|-- 1 --| 0 |          # --daystart を指定した場合


daystart とか、そこまで正確にやる必要があったのかと問われると、微妙(´;ω;`)

Rails におけるレースコンディションの例とその回避方法

最近立て続けにレースコンディション問題に遭遇したのでメモ。
レースコンディション(競合状態)とは、複数のプロセスやスレッドが共有リソースに対して何らかの操作をする際に、処理のタイミングによって結果が異なってしまう状態のこと。よくトランザクションの解説の際に銀行口座の例として紹介されるおなじみのやつです。


今回は、アプリケーションの書き方によって発生するレースコンディションと、MySQL のテーブル定義時の制約不足で発生するレースコンディションについてそれぞれ紹介したいと思います。
どちらの場合も共有リソースはDB で、条件を満たすと意図しない形でデータが保存されてしまいます。

サンプルアプリケーション

サンプルアプリケーションとして、簡単なアクセス解析機能付きの短縮URL ツールを考えます。
アクセス解析機能として、以下のような機能を持つとしましょう。

  1. URL毎 にクリック数を計測できる
  2. 最小単位として、日毎のクリック数を閲覧できる
テーブル定義
class CreatePages < ActiveRecord::Migration
  def self.up
    create_table :pages do |t|
      t.string :url, :null => false
      t.index :url, :unique => true
    end

    create_table :counts do |t|
      t.integer :click, :null => false, :default => 0
      t.date :count_date, :null => false

      t.references :page
    end
  end
  
  def self.down
    drop_table :pages
    drop_table :counts
  end
end
アプリケーションコード
class Page < ActiveRecord::Base
  has_many :counts

  def click
    Count.click(self.id)
  end
end

class Count < ActiveRecord::Base
  belongs_to :page

  def self.click(page_id)
    count = self.find_or_create_by_page_id_and_count_date(page_id, Date.today)
    count.click += 1
    count.save
  end
end

class RelayController < ApplicationController
  def index
    page = Page.find(params[:id])
    page.click
    redirect_to page.url
  end
end

サンプルアプリケーションの問題

サンプルアプリケーションの「Count.click」メソッドにはレースコンディションが発生する可能性がある部分が二カ所存在します。

1. クリック数のインクリメントにおけるレースコンディション

Count オブジェクトが既に存在する場合、「Count.click」メソッドは次のコードと同じになります。

def self.click(page_id)
  count = self.find_by_page_id(page_id)
  count.click += 1
  count.save
end


今、Rails アプリケーションのプロセスが2つ起動しているとします。そして、「RelayController#index」アクションに同時にアクセスがあった場合、次のようなことが起きる可能性があります。

  • プロセスA がcounts レコード(現在のclick の値は1)を取得
    • プロセスB がcounts レコード(現在のclick の値は1)を取得
  • プロセスA が「click += 1」を実行し、click の値は2になる
    • プロセスB が「click += 1」を実行し、click の値は2になる
  • プロセスA が更新内容をDB に書き出す
    • プロセスB が更新内容をDB に書き出す


この結果、本来「3」になるはずのクリック数が「2」となってしまいます。


このようなことが発生してしまう原因は、アプリケーション側で値のインクリメントを行い、その値で現在のレコードの値を更新しているからです。
この問題に対応するにはDB 側でインクリメント処理を行う必要がありますつまり、アプリケーションはDB に対して「インクリメントして!」という命令のみを発行します。
Rails にはまさにこの処理を行うメソッド「ActiveRecord::Base.increment_counter」が用意されています(´∀`) このメソッドは指定された値をインクリメントするSQL を発行し、MySQL の場合は次のようなクエリが発行されます。

UPDATE counts SET click = click + 1 WHERE id = 1


increment_counter を使った「Count.click」は次のようになります。

def self.click(page_id)
  count = self.find_or_create_by_page_id_and_count_date(page_id, Date.today)
  self.increment_counter(:click, count.id)
end


これならば、先程のケースの問題を回避できます。DB には値そのものを書き込むSQL ではなく、インクリメントを指示するSQL が2回発行されるからです。これでも結果が正しくならない場合、それはDB 側のトランザクションの問題やコネクションが切れた等アプリケーションの外の問題になります。

2. counts レコードの生成におけるレースコンディション

レコードの生成は次のように行っています。

count = self.find_or_create_by_page_id_and_count_date(page_id, Date.today)


指定したページと日付の組合せが既に存在すればそのレコードを使います。存在しなければレコードを作成し、作成されたレコードを使います。これによって、同じ日付のクリック数を記録するレコードが複数作成されることを防止しています。


さて、この状態で先のように同時にアクセスがあると…。予想通り1つの「pages」レコードに対して同じ日付を持つ「counts」レコードが複数生成される可能性があります。これは、「find_or_create_by」メソッドが、最初に「find」を実行し、対応するレコードがない場合に「create」を実行するからです。最初にアクセスしたプロセスが新しい「count」レコードを生成する前に、次のプロセスが「find」を実行してしまうと、どちらのプロセスも新しいレコードを生成してしまいます。


この問題に対応するにはどうしたら良いでしょうか?「validates_uniqueness_of」を使えば良いでしょうか?
答はノーです。「validates_uniqueness_of」も結局SELECT を実行してレコードが取得できなければユニークであると判定してしまうため、レースコンディションを発生させます。この「validates_uniqueness_of」の問題に関しては3 日坊主日記 - validates_uniqueness_of は信頼できない , sqlite3 - Getting Startedが詳しいです。
この問題に対応する方法の1つは「counts」テーブルにユニーク制約を追加することです。ユニーク制約を追加すれば、レースコンディションが発生した際に2番目にレコードを生成しようとしたプロセス側でユニーク制約違反が発生します。書き込み時にブロックするので、同じレコードが複数生成されるのを防止することができます。


追加するユニーク制約は次のようになります。

create_table :counts do |t|
  t.integer :click, :null => false, :default => 0
  t.date :count_date, :null => false

  t.references :page
  t.index [:page_id, :count_date], :unique => true
end

まとめ

今回は自分がはまったレースコンディションについて2つ書きました。オープンソースのコードの中にはこの問題に対応していないものもあります。rails-ad-serverの「rads_url」アクションとか。アクセス解析系ツールは同時アクセスが非常に発生しやすいアプリケーションだと思うのでこれは危険なにおいがします。
かくいう自分も、最初に「はまった」と言った通り、サンプルアプリケーションと似たようなコードを書いてしまいました。この問題を発見できたのは、テスト時に同時アクセスのベンチマークをとったためです。やはり実際に試してみると気づくことが多いなと、また1つ勉強になりました。
「counts レコードの生成におけるレースコンディション」のようにアプリケーション(フレームワーク)レイヤーのレベルでは対応できないものもありました。ORM は裏でどんな処理が走っているかを考えなくても使えてしまう便利なものですが、その処理内容と、内容によっては起こる問題をきちんと理解しておくことが大切だなと思います。

追記

id:lizy さんからブコメでツッコミをいただきました。

これはISOLATION LEVELの問題じゃないですか?1個目はREPEATABLE READがあれば大丈夫か。2個目はSERIALIZABLEが必要かな


これについて考えてみました。説明を分かりやすくするため「2. counts レコードの生成におけるレースコンディション」について先に考えます。

2. counts レコードの生成におけるレースコンディションの場合

この場合はご指摘の通りISOLATION LEVEL がSERIALIZABLE ならば防ぐことができそうです。

このレベルは REPEATABLE READ と似ていますが、InnoDB は暗黙的に全ての単純な SELECT ステートメントを SELECT ... LOCK IN SHARE MODE にコミットします。

MySQL :: MySQL 5.1 リファレンスマニュアル :: 13.5.10.3 InnoDB と TRANSACTION ISOLATION LEVEL


LOCK IN SHARE MODE で実行すると何が起こるかは、以下のマニュアルが参考になります。

共有モードで読み取りを行うというのは、まず最新の有効なデータを読み取り、そして読んだ行に共有モードを設定するという意味です。共有モード ロックは、読み取った行が別の人によって更新されたり、削除されたりする事を防ぎます。また、もし最新データが別のクライアント接続のコミットされていないトランザクションに属していたら、そのトランザクションがコミットされるまで待ちます。

MySQL :: MySQL 5.1 リファレンスマニュアル :: 13.5.10.5 SELECT ... FOR UPDATE と SELECT ... LOCK IN SHARE MODE ロック読み取り


これらをふまえて、「2. counts レコードの生成におけるレースコンディション」についてレースコンディションが発生しそうな3パターンと、それに対するISOLATION LEVEL 毎の挙動をまとめます。

  1. プロセスA が新しいレコードをINSERT する前にプロセスB がSELECT を実行した
    • REPEATABLE READ の場合、プロセスB のSELECT は空の結果を返すので、プロセスB もINSERT を実行します。結果重複レコードが生成されてしまいます
    • SERIALIZABLE の場合、プロセスB のSELECT は空の結果を返すので、プロセスB もINSERT を実行します。しかし、プロセスA, B 共に共有ロックを取得した状態なので、先にINSERT を実行した方はまずブロックされ、他方がINSERT を実行した時点で、先にINSERT を実行した方は成功し、後に実行した方はデッドロックによるエラーになります。エラーしたプロセス側でもう一度SELECT を発行した場合、他方のプロセスがトランザクションをCOMMIT していれば最新のレコードを取得でき、COMMIT されていなくてもSELECT がブロックされるので、いずれにせよレコードの取得に成功します。結果、重複レコードが生成されることはありません
  2. プロセスA が新しいレコードをINSERT したが、プロセスA がトランザクションをCOMMIT する前にプロセスB がSELECT を実行した
    • REPEATABLE READ の場合、COMMIT していない内容を他のトランザクションから見ることは防止されているので、プロセスB のSELECT は空の結果を返します。よって、プロセスB もINSERT を実行します。結果重複レコードが生成されてしまいます
    • SERIALIZABLE の場合、リファレンスにある通り、最新データが別のCOMMIT されていないトランザクション(この場合はプロセスA のトランザクション)にある場合、SELECT をブロックします。そして、COMMIT された時点も最新データが取得されるので、プロセスB のSELECT はレコードの取得に成功します。結果、重複レコードが生成されることはありません
  3. プロセスA がトランザクションをCOMMIT する前にプロセスB のトランザクションが開始していて、プロセスA のトランザクションがCOMMIT された後でプロセスB のSELECT が実行された
    • REPEATABLE READ の場合、トランザクション中のSELECT はトランザクション中で最初に実行されたSELECT と同じスナップショットを読みとるのであって、トランザクションが開始された時点でのスナップショットではありません。よって、この場合プロセスB のSELECT はレコードの取得に成功します。結果、重複レコードが生成されることはありません
    • SERIALIZABLE の場合、SELECT は最新データを読もうとし、最新データが他のCOMMIT されていないトランザクションに属する場合はSELECT をブロックします。よって、この場合プロセスB のSELECT はレコードの取得に成功します。結果、重複レコードが生成されることはありません


結論として「2. counts レコードの生成におけるレースコンディション」についてはISOLATION LEVEL がSERIALIZABLE なら防止可能でした。

1. クリック数のインクリメントにおけるレースコンディションの場合

この問題の根本は「プロセスA, B がそれぞれが同じクリック数取得してしまう」ことです。本来ならプロセスB はプロセスA がインクリメントした結果の値を取得しなければなりません。
これについても、SERIALIZABLE ならば対応可能な問題ですが、REPEATABLE READ だと問題が発生する場合があります。例を書きます。

  • プロセスA がクリック数をインクリメントし、トランザクションをCOMMIT する前にプロセスB が現在のクリック数を取得した
    • REPEATABLE READ の場合、他トランザクションのCOMMIT されていない変更を見ることができません。よって、プロセスB が取得するクリック数はプロセスA がインクリメントする前の値になります。結果、プロセスA, B 共に同じ値を新しいクリック数として書き込み、実質インクリメントは1回か行われません
    • SERIALIZABLE の場合、プロセスA がクリック数を更新しているので、プロセスB のSELECT はプロセスA のトランザクションがCOMMIT するまで待たされます。よって、プロセスB が取得できるクリック数はプロセスA によってインクリメントされた後の値となります。結果、インクリメントは正常に2回行われます


結論として「1. クリック数のインクリメントにおけるレースコンディション」についてもSERIALIZABLE なら防止可能でした。
ちなみに、この例に対してはより適切な方法があります。

もし2ユーザがカウンタを同時に読むと、少なくても1ユーザはカウンタを更新しようとする時にデッドロックになってしまう為、 LOCK IN SHARE MODE はこの場合良い解決法とはいえません。

この場合、カウンタの読み取りとインクリメントを実装する為の良い方法が2つあります:(1) カウンタを1でインクリメントする事で更新し、その後にだけ読み取る、または、 (2) ロックモード FOR UPDATE を利用してまずカウンタを読み取り、その後にインクリメントする。後者の方法は、次のように実装できます:


SELECT counter_field FROM child_codes FOR UPDATE;
UPDATE child_codes SET counter_field = counter_field + 1;


A SELECT ... FOR UPDATE は、読み取る各行上に専用ロックを設定し、最新の有効データを読み取ります。従って、それは SQL UPDATE が行上に設定する物と同じロックを設定します。

MySQL :: MySQL 5.1 リファレンスマニュアル :: 13.5.10.5 SELECT ... FOR UPDATE と SELECT ... LOCK IN SHARE MODE ロック読み取り


ここにある通り、SERIALIZABLE で対応する場合には、後にUPDATE を実行したトランザクションデッドロックによるエラーが発生します。よって、正常な動作をさせるためには、もう一度SELECT によって値を取得し直さなければなりません。
そもそもこの問題の根本は「プロセスA, B がそれぞれが同じクリック数を読みとってしまう」ことでした。よって、プロセスB のSELECT 自体をプロセスA のトランザクションがCOMMIT されるまでブロックできれば良い訳です。そして、これを実現するのが「SELECT 〜 FOR UPDATE」です。このクエリを実行したプロセス(MySQL レベルではセッション)はSELECT 対象となった行の排他ロックを取得し、トランザクション中はロックを保持します。なので、後発のSELECT は排他ロックを獲得できるまで待たされ、ブロックされたSELECT の実行結果には先にロックを保持していたトランザクションの実行結果が反映されるという寸法です。


さらにちなむと、Rails のfind メソッドは「:lock => true」というオプションを渡してやると「FOR UPDATE」付きSELECT を発行します。但し、「find_or_create_by」系のメソッドではこのオプションが無効なので使いどころは微妙かもしれません。


今回の問題について、ISOLATION LEVEL のことは考えていませんでした。ツッコミいただき感謝です!勉強になりました。

Always Hatebu Favorites が動かなくなってので対応しました

あけましておめでとうございます!!今更ながら今年もよろしくお願いしますヾ(゚∀゚)ノ
現在閲覧しているページをはてブしている「お気に入りユーザ」を表示するGreasemonkeyで公開しているGreasemonkey が、Firefox を3.6 にアップデートしたら動かなくなったので対応しました。

対応方法

  1. こちらから最新版をインストール
  2. はてなにログイン
  3. スクリプトコマンドから、update favorites を実行
  4. 次回のリクエストから機能が有効になります

原因

内部で、AutoPagerizeのソースから拝借した「createHTMLDocumentByString」という関数を使っていたのですが、これが動かなくなっていました。XHR で取得したレスポンスからdocument オブジェクトを生成してくれる関数です。そのオブジェクトに対してXPath で要素を取得とかやっていました。

対応

レスポンスを取得してXPath で解析する方法は、Greasemokey では結構使われていると思います。そこで、既に誰かが対応していると思い調査。今回はTwitter Text Converterの変更を参考にしました。
「createHTMLDocumentByString」をこちらに入れ替えて対応

JavaScript の関数はとても柔軟だった

1年以上放置していたO'Reilly Japan - 初めてのJavaScriptを最近またやり始めました。「5章 関数」を読んだので、覚えたことを書いてみます。タイトルの通り、JavaScript の関数が持つ柔軟性についてフォーカスを当てます。
なお、動作検証にはRhino - MDC(1.7 release 2 2009 03 22)を使っています。

関数がオブジェクト

関数がオブジェクトなので、変数や配列要素に関数を代入できたり、関数を引数として渡したりできる。JavaScript の関数はファーストクラスオブジェクト

  • ruby のブロックみたいに使える
js> [1,2,3,4,5].filter(function(element, index, array){ return element % 2 == 0; })
2,4


関数は() をつけると呼び出せる。() つけないとオブジェクトとして扱うことができ、返り値として使えたりする。

  • 引数として、データとそのデータを使って処理を行なう関数を渡すと関数を実行してくれる関数
js> function exec(elem, func){
  >   return func(elem);
  > }
js> exec(1, function(e){ return e * 3;})
3


すごー( ゚д゚)

  • ruby のeach みたいなのを書いてみる
js> var MyArray = function(array){
  >   this.array = array;
  >   this.each = function(func){
  >     for(var i = 0; i < this.array.length; i++){
  >       func(this.array[i])
  >     }
  >     return this;
  >   }
  > }
js> var myarr = new MyArray([1,2,3,4,5,6,7]);
js> myarr.each(function(e){ print(e); })
1
2
3
4
5
6
7
[object Object]

関数は引数の数を考慮しない

関数を定義した時の仮引数の数と、呼び出す際に渡す引数の数が異なっていてもエラーにならない。

  • 呼び出す際に値を指定しないと、その引数の値はundefined になる
js> var x = function(a, b){
  >   var sum = 0;
  >   if(a) sum += a;
  >   if(b) sum += b;
  >   return sum;
  > }
js> x()
0
js> x(1)
1
js> x(1, 2)
3
js> x(1, 2, 4)
3
  • 引数の数を考慮しないので、一番最初に示した例はこう書ける。このようなコールバック関数の時とか便利。必要なものだけ受けとれる。
js> [1,2,3,4,5].filter(function(e){ return e % 2 == 0; })
2,4
  • 関数は引数を保持するarguments というオブジェクトを持つ
js> var sum = function(){
  >   var res = 0;
  >   for(var i = 0; i < arguments.length; i++){
  >     res += arguments[i];
  >   }
  >   return res;
  > }
js> sum()
0
js> sum(1)
1
js> sum(1,2,3,4)
10


どことなく、Ruby が持つ柔軟性に似た部分を感じる。

まとめとして、prototype.js のソースの一部を読んでみる

Prototype JavaScript framework: Easy Ajax and DOM manipulation for dynamic web applicationsの1.6.0.1 のEnumerable のeach の部分のソースを読む。

1. まず元になるeach がArray にありそうなので、そこを読む

あった.。゚+.(・∀・)゚+.゚。

Object.extend(Array.prototype, {
  _each: function(iterator) {
    for (var i = 0, length = this.length; i < length; i++)
      iterator(this[i]);
  },


さっき自分で書いたのと処理的には同じ。prototype とかはまだ勉強していない。既存のクラスに関数を追加したりする際に使いそう。
length を最初に変数に代入しているのは、毎回評価することの無駄を省くためか、それともコールバック関数の内部で破壊的動作をした時にもループが最後まで回るようにするためだと思う。

2. 次にこのeach を使うEnumerable を読む

意外なことに、Enumerable にもeach が定義されていたので、今回はここを読むとする。each はArray やHash だけに定義されていて、Enumerable はそのeach を使って処理を行なうmap とかが定義されているのかと思った。
each が2つある謎にせまる。

var Enumerable = {
  each: function(iterator, context) {
    var index = 0;
    iterator = iterator.bind(context);
    try {
      this._each(function(value) {
        iterator(value, index++);
      });
    } catch (e) {
      if (e != $break) throw e;
    }
    return this;
  },


主要な部分だけ抽出すると、以下のようになる。

var Enumerable = {
  each: function(iterator, context) {
    var index = 0;
    this._each(function(value) {
      iterator(value, index++);
    });
    return this;
  },


index をEnumerable 側のeach で管理している。共通化している意味はこの辺りにありそう。一端Hash のeach を見てみる。

    _each: function(iterator) {
      for (var key in this._object) {
        var value = this._object[key], pair = [key, value];
        pair.key = key;
        pair.value = value;
        iterator(pair);
      }
    },


各要素を取得する処理にfor .. in 形式のfor を使っている。もしここでインデックスの値を扱いたいなら、この中でインデックスの値を保持する変数を用意しないといけない。他にも、Range のeach はsucc 関数を使って要素を取り出したりしているのでこの場合も別途インデックスの値を保持する変数が必要になってしまう。
この辺の処理をEnumerable のeach で共通化している。今回は省略してしまったけど、エラー処理とかもEnumerable のeach で共通化している。each はEnumerable の他の関数内で使われるのでここでエラー処理をきちんと行なっておくのは理にかなってそう。

Enumerable のeach はクロージャを使っていたり、関数が引数の数を気にしない性質を使っていたりして、これだけでも勉強になりました。これならeach_with_index いらないもんね。

COUNT 関数を使ってMySQL のインデックスの基本を理解する

Linux-DB システム構築/運用入門の8章「インデックスのチューニング(前編)」を読んだので、インデックスの基本について実際に手を動かしながら勉強してみようと思います。
内容としては、クエリを実行する際に、「インデックスだけにアクセスした場合」と、「データにもアクセスする場合」のI/O 回数の違いが、パフォーマンスにどれだけ影響を与えるか調べてみるというものです。

環境

今回は、インデックスだけにアクセスした場合と、データにもアクセスする場合のパフォーマンスの違いについて調べたいので、インデックスの構造が「キーの値, データの位置」となっているMyISAM の方が調査環境に向いていると判断しました。

  • テスト用データ
    • 600万行弱
    • インデックスなしカラムが1つ
    • インデックスありカラムが2つ
      • 1つはNOT NULL のインデックスつきカラム
      • 1つはNULL ありのインデックスつきカラム
mysql> SHOW fields FROM test_data; 
+--------------+-----------------------------------+------+-----+---------------------+----------------+
| Field        | Type                              | Null | Key | Default             | Extra          |
+--------------+-----------------------------------+------+-----+---------------------+----------------+
| id           | int(10) unsigned                  |      | PRI | NULL                | auto_increment |
| name         | varchar(255)                      | YES  |     | NULL                |                |
| key          | datetime                          |      | MUL | 0000-00-00 00:00:00 |                |
| nullable_key | datetime                          | YES  | MUL | NULL                |                |
+--------------+-----------------------------------+------+-----+---------------------+----------------+

mysql> SELECT COUNT(*) FROM test_data;
+----------+
| COUNT(*) |
+----------+
|  5967003 |
+----------+

検証したいこと

本を読んで学んだ知識を検証対象として、実際にその通りになるのか確かめてみます。検証したいことは以下の通りです。

1. インデックスだけを読む場合と、データも読む場合では実行時間にどのくらい差があるか
  • MyISAM のインデックスは「キーの値, データの位置」という構成になっているので、インデックスを使ってデータにアクセスする場合、2回のI/0 が必要になる
    • 1. インデックスから条件に一致するノードを取得
    • 2. ノードに記録されている「データの位置」を基に実データにアクセスする


この性質を利用して、インデックスだけを読む場合と、データも読む場合では実行時間にどのくらいの差があるか検証します。

2. インデックスを使った際、インデックスのキーになっているカラムだけを取得する場合は、実データへのアクセスが必要なくパフォーマンスが向上する

MySQL のインデックスは、キーの値自体をインデックスに含むので、その値だけを取得する場合は実データへのアクセスが必要ないはずです。この理解が正しいか検証します。

3. 実データにアクセスする必要がある場合、インデックスを使うよりも、フルテーブルスキャンの方が速い場合がある。また、このことをMySQLオプティマイザが判断できないことがある
  • 1 で説明した通り、実データにアクセスする際には2回のI/O が必要になる
  • この時、1回目のI/O はインデックスがソート済みで格納されているためシーケンシャルアクセスだが、実データへのアクセスはランダムアクセスになる
    • ヒットするレコードがN 件だとすると、ほぼN 回のランダムアクセスが発生する
  • 一方、フルテーブルスキャンは、全データを読み出す必要があるが、シーケンシャルアクセスである
  • 通常MySQL はI/O をブロック単位で行なうが、フルテーブルスキャンの場合は、一回のI/O で複数のブロックを一気に読むことができ、これによってI/O 回数を減らす工夫をしている


この性質から、ヒットするレコードの件数が多い場合は、インデックスを使ってランダムアクセスが多数発生するより、フルテーブルスキャンを使った方がI/O 回数が少なくなり、パフォーマンスが向上します。この動作を検証します。

検証方法

今回の検証にはCOUNT 関数を用います。

COUNT()、MIN()、SUM() などの集約(要約)関数では、NULL 値は無視されます。例外は COUNT(*) です。この関数は、個々のカラム値ではなくレコードをカウントします。 たとえば、以下のステートメントでは 2つのカウントが行われます。 最初は、テーブルにあるレコード数のカウントです。2 番目は age カラムにある非 NULL 値のカウントです。

mysql> SELECT COUNT(*), COUNT(age) FROM person;

MySQL :: MySQL 4.1 リファレンスマニュアル :: A.5.3 NULL 値の問題


ここにある通り、COUNT 関数は以下のように動作します。

  • COUNT(*) はカラムの値は見ずに単純にレコード数をカウントするので、インデックスだけを読めば結果を返すことができる
  • COUNT 関数の引数として、インデックス以外のカラムを指定すると、インデックスを辿って実データにアクセスし、そのカラムの値がNULL かどうかチェックしつつか数え上げを行う


COUNT 関数のこの性質を利用すれば、インデックスだけにアクセスする動作と、実データにもアクセスする動作を簡単にスイッチでき、その差を確認することができます。

検証

1. インデックスだけを読む場合と、データも読む場合では実行時間にどのくらい差があるか

まず、インデックスだけを読む場合。

mysql> SELECT SQL_NO_CACHE COUNT(*) FROM test_data WHERE key BETWEEN '2009-04-01 00:00:00' AND '2009-04-30 23:59:59';
+----------+
| COUNT(*) |
+----------+
|   298070 |
+----------+
1 row in set (0.19 sec)

mysql> EXPLAIN SELECT SQL_NO_CACHE COUNT(*) FROM test_data WHERE key BETWEEN '2009-04-01 00:00:00' AND '2009-04-30 23:59:59';
+----+-------------+-----------+-------+---------------+-----------+---------+------+--------+--------------------------+
| id | select_type | table     | type  | possible_keys | key       | key_len | ref  | rows   | Extra                    |
+----+-------------+-----------+-------+---------------+-----------+---------+------+--------+--------------------------+
|  1 | SIMPLE      | test_data | range | key_index     | key_index |       8 | NULL | 292889 | Using where; Using index |
+----+-------------+-----------+-------+---------------+-----------+---------+------+--------+--------------------------+
1 row in set (0.00 sec)


Extra に「Using index」が表示されているので、インデックスだけが読まれていることが分かります。


次に、インデックスとデータの両方を読む場合。
id というインデックスに関係ない値を使ってCOUNT を実行することで、実データへのアクセスが発生するようにします。

mysql> SELECT SQL_NO_CACHE COUNT(id) FROM test_data WHERE key BETWEEN '2009-04-01 00:00:00' AND '2009-04-30 23:59:59';
+-----------+
| COUNT(id) |
+-----------+
|    298070 |
+-----------+
1 row in set (2.07 sec)

mysql> EXPLAIN SELECT SQL_NO_CACHE COUNT(id) FROM test_data WHERE key BETWEEN '2009-04-01 00:00:00' AND '2009-04-31 23:59:59';
+----+-------------+-----------+-------+---------------+-----------+---------+------+--------+-------------+
| id | select_type | table     | type  | possible_keys | key       | key_len | ref  | rows   | Extra       |
+----+-------------+-----------+-------+---------------+-----------+---------+------+--------+-------------+
|  1 | SIMPLE      | test_data | range | key_index     | key_index |       8 | NULL | 292889 | Using where |
+----+-------------+-----------+-------+---------------+-----------+---------+------+--------+-------------+
1 row in set (0.00 sec)


Extra に「Using index」が表示されなくなったことから、実データへのアクセスが発生したことが分かります。
結果として、パフォーマンスに10倍以上の差がでました。id は主キーなので、COUNT(id) の結果はCOUNT(*) と同じになるのですが、MySQL が内部的にCOUNT(*) に書き換えてくれることはないみたいです。

2. インデックスを使った際、そのインデックスのキーになっているカラムだけを取得する場合は、実データへのアクセスが必要なく、パフォーマンスが向上する

インデックスのキーになっているkey を使ってCOUNT 関数を実行します。

mysql> SELECT SQL_NO_CACHE COUNT(key) FROM test_data WHERE key BETWEEN '2009-04-01 00:00:00' AND '2009-04-30 23:59:59';
+------------+
| COUNT(key) |
+------------+
|     298070 |
+------------+
1 row in set (0.20 sec)

mysql> EXPLAIN SELECT SQL_NO_CACHE COUNT(key) FROM test_data WHERE key BETWEEN '2009-04-01 00:00:00' AND '2009-04-30 23:59:59';
+----+-------------+-----------+-------+---------------+-----------+---------+------+--------+--------------------------+
| id | select_type | table     | type  | possible_keys | key       | key_len | ref  | rows   | Extra                    |
+----+-------------+-----------+-------+---------------+-----------+---------+------+--------+--------------------------+
|  1 | SIMPLE      | test_data | range | key_index     | key_index |       8 | NULL | 292889 | Using where; Using index |
+----+-------------+-----------+-------+---------------+-----------+---------+------+--------+--------------------------+
1 row in set (0.00 sec)


EXPLAIN に「Using index」が表示されていることから、このクエリの結果を取得する際にはインデックスにだけしかアクセスしていません。実データへのアクセスは発生してないことから、仮説が正しいことが分かりました。インデックスにだけしかアクセスしていないため、クエリの実行も高速でした。
また、COUNT(key) としたのでCOUNT(*) の時と比べて、NULL 値チェックが入りますが、そのオーバーヘッドはほとんどないことも分かりました。若干あったとしても、I/O が発生する場合の処理に比べれば、実行時間への影響は小さいものです。


この結果より、SELECT 時に取得する全てのカラムが、そのクエリで使用されるインデックスの構成要素になっている場合、実行時には、実データへのアクセスが発生しないことが分かります。
僕は前まで、SELECT 時に取得するカラム数を少なくすることで、SELECT を高速化することができると思っていました。でも実際には、実データへのアクセスを発生させないようにカラムを選択することが重要で、それによってI/O 回数を減り、パフォーマンスが向上するということを理解することができました。

3. 実データにアクセスする必要がある場合、インデックスを使うよりも、フルテーブルスキャンの方が速い場合がある。また、このことをMySQLオプティマイザが判断できないことがある

COUNT(id) を使って実データへのアクセスを発生させると共に、結果がテーブルのレコード数の70% を占めるようなクエリを発行します。

まず、インデックスを使用する場合。

mysql> SELECT SQL_NO_CACHE COUNT(id) FROM test_data WHERE nullable_key IS NULL;
+-----------+
| count(id) |
+-----------+
|   4247195 |
+-----------+
1 row in set (25.38 sec)

mysql> EXPLAIN SELECT SQL_NO_CACHE COUNT(id) FROM test_data WHERE nullable_key IS NULL;
+----+-------------+-----------+------+--------------------+--------------------+---------+-------+--------+-------------+
| id | select_type | table     | type | possible_keys      | key                | key_len | ref   | rows   | Extra       |
+----+-------------+-----------+------+--------------------+--------------------+---------+-------+--------+-------------+
|  1 | SIMPLE      | test_data | ref  | nullable_key_index | nullable_key_index |       9 | const | 878918 | Using where |
+----+-------------+-----------+------+--------------------+--------------------+---------+-------+--------+-------------+
1 row in set (0.00 sec)


EXPLAIN のkey の部分を見ると、インデックスが使われていることが分かります。しかし、クエリの実行には25秒もかかってしまいました。


次に、IGNORE INDEX ヒントを使って、フルテーブルスキャンで同じクエリを実行してみます。

mysql> SELECT SQL_NO_CACHE COUNT(id) FROM test_data IGNORE INDEX(nullable_key_index) WHERE nullable_key IS NULL;
+-----------+
| count(id) |
+-----------+
|   4247195 |
+-----------+
1 row in set (13.21 sec)

mysql> EXPLAIN SELECT SQL_NO_CACHE COUNT(id) FROM test_data IGNORE INDEX(nullable_key_index) WHERE nullable_key IS NULL;
+----+-------------+-----------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
+----+-------------+-----------+------+---------------+------+---------+------+---------+-------------+
|  1 | SIMPLE      | test_data | ALL  | NULL          | NULL |    NULL | NULL | 5967003 | Using where |
+----+-------------+-----------+------+---------------+------+---------+------+---------+-------------+
1 row in set (0.00 sec)


なんと、フルテーブルスキャンで実行した方が、半分の時間でクエリを実行することができました。やはり、ヒットするレコードの件数が多い場合、インデックスを使ってしまうとランダムアクセスが多数発生し、フルテーブルスキャンよりパフォーマンスが落ちてしまいました。
また、IGNORE INDEX ヒントを使わないと、オプティマイザが効率の悪い実行計画を立ててしまうことも分かりました。

まとめ

今回、「Linux-DB システム構築/運用入門」の8章を読んで、MySQL のクエリチューニングをする際には、EXPLAIN を見ながらインデックスが使われるようにクエリを組み立てること以上に、I/O 回数をいかに減らすかという点が重要だということを理解することができました。


特に、今回の最後の検証で扱った例では、ランダムアクセスの効率がいかに悪いのかを実感することができました。今までフルテーブルスキャンなんて最悪と思っていた僕は、その奥にあるI/O、ディスクのシーク時間を意識することができていませんでした。その本質を理解することができ、とても大きな学びを得たと実感しています。松信本スバラシ。


最後にp.182 の本文からちょっと引用(一部改変)して、エントリーを終わります。

RDBMS のパフォーマンスの支配要因となるディスク性能を考える上で、1回のI/O で転送する量よりも、I/O 回数のほうがずっとインパクトが大きい。例えば、1KB ずつ10,000回読むのと、10KB ずつ1,000回読むのとでは、同じデータ量を読み込むにもかかわらず、後者の方がずっと高速になります。これは、一回のI/O のたびに(キャッシュされていなければ)ディスクのシーク、回転待ちが発生するためです。

REST について調べたまとめ

普段仕事でRails を使っている身ですが、Rails 2.x 系を使っているものが1つもない。結構前からRails 3 の話題がでてきている今、そろそろRails 2.x をまともに使っておきたいと思ったので、まずはREST について調べました。最初にREST について調べたのは、REST がRails 2.x (実際には1.2.x から)で導入された最も大きな概念だからです。

REST とは

REST とはアーキテクチャスタイルである

アーキテクチャスタイルとはデザインパターンのようなもので、システムを設計する上での方針をまとめたものである。

REST は「REpresentational State Transfer」の略である

直訳すると、「(リソースの)表現可能な状態の転送」。あるリソースの状態を表現したものがサーバからクライアントに転送されるのがREST。ここにでてきた「リソース」「表現」「状態」は全てREST において重要な概念なので後述する。
Web を例にすると、「商品在庫一覧ページ」は、商品という「リソース」の在庫であるという「状態」をHTML 形式で「表現」したものと見なすことができる。この表現(HTML ファイル)をサーバがブラウザに「転送」している。これがREST の概念である。

REST はRoy Fielding によって提案された

Roy Fielding はHTTP プロトコルの作成者の1人であり、REST はWeb と密に関係している。

REST とWorld Wide Web

REST の提案者Roy Fielding はWeb 及びHTTP の構造を調査する過程の中で、ネットワークアーキテクチャに共通して適応できる原理として抽出したものをREST として発表した

よって、REST は主にWorld Wide Web とHTTP に適用され、Web を組織化、構造化するための最善の方法に対する統一理論である。実際に、Web ではREST に準拠した通信が可能である。一方、REST の原理に違反することも可能である。

REST とWeb アプリケーション

Web アプリケーションを設計する際にもREST を使うことができる

HTTP 層と関係する部分、すなわち、クライアントとやりとりする部分の設計に適応することができる。具体的に、次に示すようなものにおいて、REST を適応できる。

  • URL の定義
  • HTTP メソッドの使い方
  • HTTP レスポンスステータスコードの使い方
  • HTTP キャッシュの利用

REST における重要要素

リソース
  • リソースに関する明確な定義は存在しない
    • 最も一般的な定義は「アイデンティティを持つ(= 名前をつけることができる)何か」である
  • リソースは「名詞」で表現することができる
  • 処理や振る舞いはリソースではない
    • 例えば、「登録フォーム」というリソースは存在するが、「登録を処理する」というリソースは存在しない
  • リソースの例として以下のようなものがある
    • 電子文書
      • 例えば、このエントリー
    • 画像
    • 商品
      • 例えば、りんご
    • サービス
      • 例えば、現在の日本の時刻
状態
  • リソースの状態
  • 状態の例として以下のようなものがある
    • (商品の)在庫
    • (作成前の)商品
      • 商品登録しようとしてフォームを開いている時の状態。Rails 上では「/products/new」というURL で表現される
URL
  • Web 上にあるリソースを一意に特定するもの
  • URL から見れば、リソースとはネットワーク上でアドレス指定が可能なもの


「リソース」「状態」「URL」の3つについて書きましたが、ここから、REST において「リソース」とは、最初に説明したような「(名詞のみで表現される狭義の)リソース」と、その「状態」を合わせたものと見てよさそうです。つまり、「/products/new」は「作成前の商品」というリソースだと見なすことができ、URL で指定することが可能です。

表現
  • リソースの表現
  • REST では、リソースとその表現は明確に区別される
  • 通信を通じてやりとりされるものは、リソースではなくその表現
    • レスポンスを通じて取得できるものも当然リソースの表現であり、クライアントも表現を送信する
  • 現時点で、表現は以下のどちらかの方法で指定することができる
    • 「index.html」のような拡張子
    • HTTP のAccept ヘッダ
  • 表現の例として以下のようなものがある
HTTP メソッド
  • リソースに対する操作を表現する動詞として使用される
  • REST では、HTTP メソッドは制限された領域である
    • 表現はコンテンツタイプとして拡張可能であり、リソースは無限に存在するが、動詞としてのHTTP メソッドは固定されている
    • WebDAV 等による拡張されたメソッドは使用するべきではない
  • REST では、URL で表現された「リソース」に対して「HTTP メソッド」でアクションを指定することでサーバとやりとりする
  • REST において使用できるHTTP メソッドは以下の4つである

以下、説明において「これ」はリクエストのボディを表し、「そこ」は操作を行うURL を表す。

    • GET
      • 「そこにあるものを取得する」
      • GET は副作用がない、つまりセーフであると位置付けられており、リソースを作成、更新する目的には使われない
      • GET はべき等であり、他のアクションによってリソースが更新されない限り、アクションを何度繰り返しても得られる結果(アクション実行後のリソースの状態とレスポンス)は同じである
    • PUT
      • 「これをそこに格納する」
      • PUT はべき等である
    • DELETE
      • 「そこにあるものを削除する」
      • DELETE はべき等である
    • POST
      • 「そこにあるものにこれを処理させる」
      • POST はセーフでもべき等でもない
      • POST は何でもできる最終手段のメソッドであり、上記の3つのメソッドで行えないことのみを担当する
      • POST を使う代表的な例は、「新しいリソースの作成」と「既存リソースへの注釈」である
レスポンスステータスコード
  • サーバはクライアントのリクエストに対するレスポンスに適切なレスポンスステータスコードを返さなければならない
  • レスポンスステータスコードの例として以下のようなものがある
    • 200
      • リクエストが成功したことを示す
    • 201
      • リクエストの結果、新しいリソースが作成されたことを示す
    • 404
      • リクエストさえたリソースが存在しないことを示す
    • 422
      • クライアントから送信されたエンティティが意味的に無効であったことを示す
      • 例えば、金額に対し負の値を送信したような場合のレスポンスは422 になる
      • 422 はHTTP/1.1 の標準の仕様には含まれていないが、Roy Fielding 自身もこのような場合に最も適切なステータスコードは422 であると認めている


ちなみに、Rails もvalidation 失敗に対するステータスコードを422 としている。

REST のステートレス性

REST とCookie

この節に関しては「である」調では書きません。このトピックはREST の中でも議論に上ることが多いので、明確に記述することが難しいためです。あくまで調べた上での「自分の」理解としてまとめておきます。間違いに気付いたら都度直していきます。


REST の原則の1つに「ステートレス性」があります。そもそもHTTP はステートレスなプロトコルなので、この原則は自然です。
ただし実際には、多くのWeb アプリケーションではCookie を使ってセッションを実現していて、厳密にはREST に従っていません。例えば、認証が必要なページのURL に対してCookie なしにリクエストを送信した場合、大抵は認証ページにリダイレクトされてしまいます。これは、サーバが同じURL に対して異なる表現を返していて、そのURL はアドレス可能性を失っていることになるからです。だからと言って、REST の原則に従うようにセッション付きURL を使うことはセキュリティの観点から無理なわけです。
では、Cookie を使うことを前提に、それでもなるべくREST に違反しないようにするにはどうしたらいいでしょうか。「実践 Rails p.209」には以下のように書かれています。

REST によれば、アプリケーション状態はすべてクライアント側で管理しなければならない。これがステートレスの意味することである。つまり、アプリケーションに状態がないのではなく、リクエストをそれぞれ独立させることができる。クライアント/サーバセッション自体は状態を維持しない。


これに対する1つのアプローチがRails におけるCookie ベースでのセッション管理だと思います。Rails 2.x はデフォルトでセッションデータそのものをCookie に格納します。つまり、状態をなるべくクライアントに持たせる実装になっています。これによって、サーバ側がそのCookie が有効かどうかの状態を管理する必要がなくなります。この認証は、HTTP に組み込まれているBasic 認証等のようにステートレスな認証に近い形になります。デメリットとして、セキュリティ面での問題が起こりやすくなります。


さらに同書には、原則から少し外れるアプローチも書かれていました(具体的には「p.211 なぜステートレスなのか」)。そこによれば、ステートレスのそもそもの目的は「シェアードナッシングアーキテクチャによるスケーラビリティの確保」にあります。つまり、スケーラビリティをなるべく確保できる方向に設計することが大切。
その実現方法の1つとして、スティッキーセッションが上げられていました。スティッキーセッションならば、リクエストを処理するサーバをスケールアウトしていくことは可能です。サーバを増やした際に無効になるセッションが存在する可能性はありますが、認証のためだけに用いられているセッションならば許容できる場合が多いはずです。

REST を使うことのメリット

良い設計原理への誘導
  • 基本的なHTTP メソッド以外を使用する必要を感じた場合、それはリソースが隠れていることを示唆する
  • REST の原理に従うことで感じる苦痛は全て、Web の自然なアーキテクチャとして設計が間違っていることへの暗示だと捉えるべきである
  • REST の設計ではアーキテクチャ上の決定は名詞の領域に集まる。名詞の選択はドメイン主体となる傾向にあるが、RPC では実装上の詳細である手続きがインターフェースとして公開されるため、より実装の変更による影響を受けやすい
統一性
  • REST が実現する統一性は標準化作業に大きく貢献する。よってシステム間での連携がしやすい
  • 動詞としてのHTTP メソッドは全てのアプリケーションドメインで統一されている
    • これは、標準化の問題が「コンテンツタイプ」と「名詞」というアプリケーションドメイン内では標準化しやすい領域に限定されることに役立つ
  • Web 上の重要なすべてのものがURL で指定できる
    • 例えば、はてなブックマークの件数画像を取得するAPI はHTTP ベースなので、「img タグ」を使って利用することができる。これはとても簡単である
スケーラビリティ
  • ステートレスを原則とするREST では、クライアント - サーバ間で何も共有しない。これは負荷に応じて水平方向へスケールしやすいことを示す
キャッシュ
  • REST を通じてHTTP キャッシュを利用することができる
  • HTTP キャッシュの要件には「透過性」があり、キャッシュが関与しているいないに関らず転送される情報が同じになることが意識されている

REST を使う上で

ここまではREST の概念を中心に書いてきましたが、残りはREST を使う上で身につけなければいけない知識や考え方、REST について調べた上での感想について書いていきたいと思います。

リソースモデリング

REST に従ったアプリケーション、すなわちRESTful アプリケーションの設計はURL の設計が大きな部分を占めることになります。リソースに対するメソッドの設計も名前も既に決められているからです。REST においては、適切なリソースをどう導出するかが重要であり、この過程をリソースモデリングといいます。
アプリケーションの設計では、「何をやるか」という処理の方に意識が向けられがちですが、REST においては、「その処理によって得られるリソースは何なのか」を意識する必要があります。これを「Resource Oriented Architecture」といいます。
例えば、検索をしたいなら、「検索結果」というリソースに対してGET します。また、ログインは、セッションというリソースに対するCreate であると捉えます。最後の例では、セッションはDB に格納されないものである点も注目です。リソースとは必ずしもDB に格納されているものだけではないことを示しています。

リソースレイヤーの増加

リソースの概念が導入されたREST では、リソースレイヤーが従来のレイヤーに加わることになります。従来のレイヤーとは、UI 層、DB 層とその間を繋ぐプログラム層から成ります。リソース層はUI 層とプログラム層の間に入ることになり、これは以前、プログラム(オブジェクト)層とDB 層の間であったインピーダンスミスマッチが、リソース層とプログラム層で発生する可能性があることを示しています。
例えば、検索結果というリソース、確認画面に対するリソースをどう表現するのかについて悩むことになります。さらに、リソース層、DB 層での操作がCRUD しかないことから、その間を繋ぐプログラム層の設計がより重要になってきます。

HTTP メソッドに対する正しい設計

PUT はべき等です。特定のページに対するブックマークの追加を、既存のリソース(そのページのブックマーク数)に対して+1 をPUT するように実現してはいけません。何故なら、PUT リクエストをする度にブックマーク数が増えてしまい、べき等ではなくなるからです。この場合、「そのページのブックマーク数を11 で更新する」のように実現する必要があります。
PUT を使うべき場所でPOST を使ってしまう場合もあります。Amazon S3 では、バケット(ファイルシステムディレクトリのようなもの)に新たなオブジェクトを追加する際にPUT メソッドを使います。REST ではリソースの作成にPUT を使用することができます。PUT を用いる場合というのは、リソースのURL をクライアントが既に知っている場合です。S3 に追加するオブジェクトのキーも値もクライアントによって決定されるので、この場合はPUT が適切なメソッドとなります。一方、商品が「/products/123」のようなURL で表現されるなら、新しい商品を追加するような場合は、POST メソッドを用いることになります。クライアントは新しく作成される商品を表現するURL を知らないからです。

雑感

今回ざっくりREST について調べましたが、とても面白かったです。
まず、REST によってもたらされる制約が面白い。設計を良い方向に導く制約はRails に似たものがあり、Rails ユーザとしてはその効果に期待せずにはいられません。アプリケーションに関わるエンジニアの設計視点が統一されるのも良いことだと思います。過去に、URL にadd 等の動詞を入れてしまったり、delete とdestroy、confirm とconfirmation 等同じことを意味するであろう操作に異るURL が割り振ってしまい、その結果、そのURL に対応するメソッド名の一貫性を失なわせてしまった経験がある身としては、REST の原則は非常に良いと感じます。

他にも、Rails と絡めて感じたことは、コントローラ層がリクエストのメソッドを意識しなくてよくなったのがとても良いと思いました。今まではそのためにverify を使っていたり、ポストバックによって1つのメソッドに2つの役割を持たせたこともありました。これからは、URL とHTTP メソッドによってアクションが定まるので、そういった心配はなくなり、コントローラ層がすっきりします。あ、before_filter が増えるのは若干微妙な気もしますが…。親リソースをロードするためには仕方ないのかなぁ。あれはDRY というか、Once And Only Once に近いし、あまり好きにはなれない。

REST を勉強してさらに良かったのは、設計に対する意識が高まったことです。例えば、今までは、ActiveRecord を継承したモデル内に、モデルのレコードを調査してメールを送信するバッチ用メソッドを生やしたりしていました。でも、モデルの仕事はせいぜいバッチ対象となるレコードを取得する部分くらいであって、メール送信はそれこそサービス層でやるのが正しいんだと思います。DB の1テーブルを司るクラスの責任にメール送信があるとは思えません。
と、そんなことを考えながら本を読んでいました。何はともあれ、このような新しい概念を使っていく上で学ぶことができるRails はやはり素晴しいフレームワークだなぁと思いました。

Emacs からVim へ移行しました

今までCarbon Emacs を使っていたのですが、カスタマイズによって動作が重くなったり少し不満に思っていました。そうした中、Emacs23 がリリースされたり、Carbon EmacsからCocoa Emacsへの流れも来ていました。
僕もCocoa Emacs に移行しようと思ったのですが、Web+DB の記事やRails ユーザにVim を使う人が多いことを知り、ふと食わず嫌いだったVim を試してみようと思いました。
少しカスタマイズしてある程度使えるレベルになったので、まとめておきたいと思います。

環境

Vim のバージョン

Mac OS X 用 Vimのいろいろを見ると分かる通り、Macvim を使うにも色々種類があります。
僕の場合は、ターミナルと別に使いたかったのでGUI 版。そして、開発が盛んで、日本語環境で使う上で便利な設定が追加されているmacvim-kaoriya を選択しました。
以前はinsert mode に入る際に自動的にIM がON になったり(iminsert=0 が効かない)したらしいのですがこのリリースで改善されていました。

デフォルトのVim で困ったこと

最初からシンタックスハイライトとか効いているし(そうコンパイルされているからですが)、入力時にインデントを考えてカーソル位置を調整してくれたりと、なかなか使いやすいと思ったのですが、少し使っている内に何点か使いにくいと思う部分がありました。
以下詳細です。

1. バッファ切り替えが面倒

    • ls でバッファ一覧を確認して、b # で切り替え
    • Emacs のC-xC-b みたいにコマンドラインバッファに現在のバッファ一覧がでてきて、その中から選択したい

bufexplorer.vim で解決。C-l でバッファ一覧がでるようにしている。


2. session.el のように最近開いたファイルや実行したコマンドの履歴を保存しておく仕組みが欲しい

fuzzyfinder.vim(の紹介記事)YankRing.vimで解決。fuzzyfinder はEmacsanything.el ライクなインクリメンタル検索ができて凄く便利。anything.el をファイル探索やコマンド探索くらいしか使っていなかった自分にとっては代替として十分な機能。


3. insert mode まわり

    • カーソルキーで移動するのが嫌
      • vim に慣れた人は移動時にはnormal mode に戻してから移動するのか分からないのですが、insert mode のままでもカーソルキーなしで移動したい
    • C-d でDelete にならない
    • C-a(Home), C-e(End) も欲しい

この辺りはキーマッピングを変更して対応。insert mode 中の移動については、上下左右を「C-k」「C-j」「C-b」「C-f」にあてました。vimの「hjkl」に対応させようと思ったのですが、「C-h」のBackSpace と被るので左右はEmacs のバインドを採用…。


4. OS Xクリップボードと連携したい

    • デフォルトだとyank したものをブラウザのフォームにコピーとかできない

ぼちぼち散歩 MacのVimでシステムのクリップボードとやりとりするでできた。MacVim だと簡単だった。


設定ファイル

具体的な設定ファイルは以下になります。主に春なのでemacsからvimに乗り換えてみました « ふぃふmemoとWeb+DB vol.52 の記事を参考にして、なるべくカスタマイズしないようにしています。

  • .vimrc
" for macvim-kaoriya http://code.google.com/p/macvim-kaoriya/

" set nocompatible " vim の機能を使う

" <status line>
set laststatus=2 " 常にステータスラインを表示
set statusline=%<%F\ %r%h%w%y%{'['.(&fenc!=''?&fenc:&enc).'\|'.&ff.']'}\ \ %l/%L\ (%P)%m%=%{strftime(\"%Y/%m/%d\ %H:%M\")} 

" <encoding>
set encoding=utf-8
set fileencodings=euc-jp,cp932,iso-2022-jp

" <basic>
let mapleader = ","            " キーマップリーダー 
set nobackup                   " バックアップ取らない
set hidden                     " 編集中でも他のファイルを開けるようにする
set formatoptions=lmoq         " テキスト整形オプション,マルチバイト系を追加
set vb t_vb=                   " ビープをならさない
set backspace=indent,eol,start " バックスペースでなんでも消せるように
set autoread                   " 他で書き換えられたら自動で読み直す
set whichwrap=b,s,h,l,<,>,[,]  " カーソルを行頭、行末で止まらないようにする
set scrolloff=5                " スクロール時の余白確保

" <display>
set showmatch         " 括弧の対応をハイライト
set number            " 行番号表示

" <indent>
set expandtab     " tab をスペースに展開
set shiftwidth=2  " 自動インデントの幅


" <search>
set wrapscan     " 最後まで検索したら先頭へ戻る
set ignorecase   " 大文字小文字無視
set smartcase    " 大文字ではじめたら大文字小文字無視しない
set noincsearch  " インクリメンタルサーチOFF
set hlsearch     " 検索文字をハイライト


" <keymapping>
" 行単位で移動(1行が長い場合に便利)
nnoremap j gj
nnoremap k gk

map! <C-a> <Home>
map! <C-e> <End>

nnoremap <Space>. :<C-u>edit $MYVIMRC<CR>
nnoremap <Space>s. :<C-u>source $MYVIMRC<CR> :<C-u>source $MYGVIMRC<CR>
nnoremap <Space>w :write<CR>
nnoremap <Space>d :bd<CR>
nnoremap <Space>q :q<CR>
nnoremap <C-h> :<C-u>help<Space>
nnoremap <C-h><C-h> :<C-u>help<Space><C-r><C-w><CR>
nnoremap ; :
nnoremap : ;

" yank, paste with os clipboard http://relaxedcolumn.blog8.fc2.com/blog-entry-125.html
noremap <Space>y "+y
noremap <Space>p "+p

inoremap <C-d> <Delete>
inoremap <C-f> <Right>
inoremap <C-b> <Left>
inoremap <C-j> <Down>
inoremap <C-k> <Up>

" <command>
" insert mode
" \date で日付
inoremap <Leader>date <C-R>=strftime('%Y/%m/%d (%a)')<CR>


" <completion>
set wildmenu           " コマンド補完を強化
set wildmode=list:full " リスト表示,最長マッチ
set history=1000       " コマンド・検索パターンの履歴数

" <autocommand>
augroup BufferAu
    autocmd!
    " カレントディレクトリを自動的に移動
    autocmd BufNewFile,BufRead,BufEnter * if isdirectory(expand("%:p:h")) && bufname("%") !~ "NERD_tree" | cd %:p:h | endif
augroup END

augroup Chalow
  autocmd!
  autocmd BufWritePost $HOME/var/log/changelog/Changelog silent :!$HOME/bin/chalow
augroup END

" <plugins>
" BufExplorer
nnoremap <C-l> :BufExplorer<CR>

" FuzzyFinder
nnoremap <silent> fb :<C-u>FuzzyFinderBuffer!<CR>
nnoremap <silent> ff :<C-u>FuzzyFinderFile! <C-r>=expand('%:~:.')[:-1-len(expand('%:~:.:t'))]<CR><CR>
nnoremap <silent> fm :<C-u>FuzzyFinderMruFile!<CR>
nnoremap <silent> fc :<C-u>FuzzyFinderMruCmd<CR>

" Vim/Ruby
" http://blog.blueblack.net/item_133
" Rubyのオムニ補完を設定(ft-ruby-omni)
let g:rubycomplete_buffer_loading = 1
let g:rubycomplete_classes_in_global = 1
let g:rubycomplete_rails = 1

" rails.vim
let g:rails_level = 4
let g:rails_devalut_database = 'mysql'

" vimwiki
let g:vimwiki_list = [{'path': '~/var/vimwiki/', 'path_html': '~/Sites/vimwiki/'}]


vim 系の記事を見てこれは便利というのを少しずつ集めました。Space をprefix にした「w」「q」「bd」は便利。insert mode 中のCtrl を使った移動もなんだかんだ便利です。
nocompatible をコメントアウトしているのは、Vim-users.jp - Hack #1: Vimを使うために必要な最小限の設定にある通り、「~/.vimrc」が存在するればvim の機能が有効化されるためです。nocompatible はvim の機能を有効化する以外にも副作用が大きいとのこと。
あとは、rubyrails を主に使うのでその設定。

  • .gvimrc
colorscheme molokai          " http://winterdom.com/2008/08/molokaiforvim 
set guifont=Osaka-Mono:h14   " フォント
set antialias                " アンチエイリアシング
set transparency=15          " 半透明
set guioptions-=T            " ツールバー削除

" カーソル点滅無効化
set guicursor=a:blinkon0

" 自動的にIM をオンにする機能の禁止
set imdisableactivate

" ウィンドウ
set sessionoptions+=resize " 行・列を設定する

if hostname() == 'macbook'
  set lines=48             " 行数
  set columns=160          " 横幅
else
 " setting for iMac
endif

set cmdheight=1            " コマンドラインの高さ
set previewheight=5        " プレビューウィンドウの高さ

set splitbelow             " 横分割したら新しいウィンドウは下に
set splitright             " 縦分割したら新しいウィンドウは右に
set showtabline=2          " タブを常に表示


カラースキームはTextMate のmonokai 調のやつを使っています。ただ、文字が切れてしまい「a」と「d」の区別がつきにくかったりするので、italic の設定は全て削除しました。

plugin

今入れているプラグインは以下の通りです。vim でのオススメ plugin - 川o・-・)<2nd life を主に参考にしました。

  • Align
    • Emacs のM-x align みたいにテキスト整形してくれます。あると地味に便利ですよね
  • bufexplorer
    • バッファのエクスプローラEmacs のC-x C-b に近い感じでバッファ切り替えができる。その場でバッファを消したりもできる。
  • yankring.vim
    • ヤンクの履歴管理。vim を終了した後もきちんと保存してくれる。
  • taglist.vim
    • ctags との連携。タグジャンプや開いているファイルのメソッド一覧を別バッファに表示してそこからジャンプとかできる。メソッド一覧を表示する際にウィンドウが狭くなってしまうのが若干困る。C-], C-t で移動できたりするのは便利。
    • OS X のデフォルトのctags だと「-R」オプションが効かないので、macports 等からctags を入れる必要がある

vim を使ってみて

今は使いながら操作を勉強しているという感じです。Web+DB の記事や、Vim-users.jpのエントリーを最初から読んで、そこからリンクを辿ったりしています。あとはVimの極め方に習いhelp を引くことに慣れるよう意識しています。vim のhelp はめちゃ見やすいし分かりやすくて驚きます。Kaoriya 版は日本語ヘルプですし。plugin にもhelp がついてくるものが多く理解するのに助かります。Emacs の時はヘルプ引くことがほとんどなかったのですが、vim のhelp は感動レベル。
vim の使用感についていいなーと今感じているのは以下の点です。

  • 軽い

gvim でもサクサク動きますね。コンソール版だとそれを上回る速さでビックリしますが、十分な軽さです

  • help がいけてる

ドキュメントが手元にあるのはとてもうれしいです。タグジャンプできるし使いやすい。plugin にhelp を含める文化があるのもいいですね。ググるよりhelp 見た方が速いです

  • オペレータ、モーション、テキストオブクジェクトが賢い

オペレータで動作を指定して、モーションで範囲を指定というのが面白いです。削除は「d」、対象がカレント行ならオペレータと同じキー(ここではd)を、行末までなら「$」を、次の段落までなら「}」をという組み合わせで操作を行えます。HTML を編集している時もタグで囲まれている中身を削除「dit」とか。慣れてくるとビジュアルモードで範囲選択して…とかしなくなりそうです。

  • 設定ファイルが分かりやすい

help があるのもありますが、emacs-lisp と比べるとやっぱり分かりやすいです。「+ruby」でコンパイルすれば設定ファイル内にruby コードを書けたりして便利。MozRepl と連携する時とかに便利。perl でもpython でも同じようなことができます。


まだflymake の代替にあまりいいのがない(errormarker.vim + 書き込み時に評価とかかなぁ)とか、Emacs で使っていた機能を全て網羅できているわけではないのですが、かなり使いやすいです。このままオライリーのvi 本とかも読んで勉強していきたいと思います。