SyntaxHighlighter

ラベル Rails の投稿を表示しています。 すべての投稿を表示
ラベル Rails の投稿を表示しています。 すべての投稿を表示

2023年11月27日月曜日

SQLite3 のトランザクションと Rails

最近は開発よりもマネージメントの仕事が多いのですが、久しぶりに技術的な話で書くネタができたので書いておきます。

SQLite3 のトランザクションは他のデータベースと少し異なり特殊なので、色々とはまりがちですが、その内容についてまとめておきます。
Rails での話も少し書いておきます。

SQLite3 の分離レベル

SQL の分離レベルは、 Wikipedia の記事にあるように、 Read Uncommitted, Read Committed, Repeatable Read, Serializable の4種類があります。

SQLite3 では、 Read Uncommitted になる例外もありますが、基本的には常に Serializable です。
後述するトランザクションのモードとして、 Deferred, Immediate, Exclusive が指定できますが、それとは関係なく、常に Serializable になるようです。
つまり、どのトランザクションのモードを選んでも、 Dirty Read や Phantom Read などは起こらず、トランザクション中は、常に整合性の取れた読み取りができるかエラーになるかのいずれかになります。

SQLite3 のトランザクションのモードと、エラーになるケース

SQLite3 では、トランザクションのモードを BEGIN TRANSACTION の際に指定することができます。

公式のページに書かれていますが、 Deferred, Immediate, Exclusive の3つを指定でき、デフォルトでは Deferred が使われます。
細かな定義については公式のページに記載があるので割愛しますが、それぞれで、「ロックを取得するタイミング」、「取得するロックの種類」が異なります。
これらは、既に実行中のトランザクションが別で存在する場合に、「相手のトランザクションの終了をどのタイミングで待つか」や「何をしたときにエラーにするか」が異なります。

例えば、標準の設定である Deferred を選んだ場合、 BEGIN DEFERRED TRANSACTION を実行したタイミングでは何のロックも取得されず、その後に続く SELECT や INSERT などが実行された際に、初めて読み取り用の Shared ロックや書き込み用の Reserved ロックが取得されます。
なお、 Shared ロックと異なり、同時に複数のセッションが Reserved ロックを取得することはできません。
そのため、例えば下記のようなコードをほぼ同時に二つ実行すると、後から実行した方は INSERT 実行のタイミングで Reserved ロックの取得に失敗することになります。

これは、 SELECT 実行の時点で Shared ロックが取得され、 INSERT 実行の時点で Reserved ロックの取得が試みられることによります。
Reserved ロックは同時に一つのセッションしか取得できないので、後から実行した方のコードでは、 Reserved ロックの取得ができません。
このタイミングで、後から実行した方のコードは、トランザクションの実行に失敗します。
勘違いしやすいですが、自動のリトライは走らず Busy Timeout の設定も意味を持ちません。
これは、後から実行した方のコードでも、既に SELECT を発行しているために、このトランザクションの Serializable の分離レベルを保証できなくなるからです。
よって、 INSERT 実行のタイミングで、デッドロックにより失敗します。

一方で、後から実行した方のコードで、 SELECT などの読み取りのみを行うのであれば、トランザクションは最後まで問題なく実行されます。
おそらくですが、 Deferred のトランザクションが最も効果的に動作するのはこのような読み取りの場合で、この場合は Phantom Read などの影響を受けずに読み取れることが保証できます。

なお、 Immediate を指定したトランザクションの場合、 BEGIN IMMEDIATE TRANSACTION を実行したタイミングで即座に Reserved ロックが取得されるので、同時に二つのコードを実行してもエラーは発生しません。
ただし、 SELECT も待たされてしまうので、並列性は下がることになります。
同時に読み込みや書き込みがあってもエラーとなる可能性は下がるので、こちらが適しているケースも多々あるように思います。

Ruby on Rails での挙動

Ruby on Rails では、標準では SQLite3 がデータベースとして使われるように設定されています。

一方で、 SQLite3 独自のトランザクションのモードである Deferred や Immediate は、 Rails の世界からは容易には指定することはできません。
原則として常に Deferred になります。
なお、分離レベルとしては、 Read Uncommitted は指定することができるようになったようです。

いずれにしても、 SQLite3 は基本的には分離レベルは Serializable であり、その上で Deferred, Immediate などのトランザクションの種類を指定することになるので、他のデータベースも考慮して、汎用的な Rails のコードを書こうと思うと、トランザクションのモードを指定するのは難しくなります。

sqlite3 ライブラリでの吸収

こちらについて少し考えた結果、 SQLite3 へアクセスするためのライブラリ sqlite3 で吸収するのがよいのではないかと考えました。

そこで、こちらの Pull Request で、「標準で使うトランザクションのモード」をコンストラクタで指定できるようにしてもらいました。

これにより、トランザクション実行時に、どのようなモードにするかの既定値を指定することができるようになります。
Rails では、何も指定せずにトランザクションが開始されるので、そういった状況で使われるモードを指定できることになります。

Ruby on Rails での設定

以上を踏まえ、 sqlite3 のバージョン 1.6.9以降を使った上で、以下の設定値を config/database.yml に書くと、トランザクションが Immediate モードで実行されるようになります。

2014年6月22日日曜日

Rails で CSRF トークン検証エラーが出ることがある

Rails で CSRF トークン検証エラーが出ることがある

自分で作った Rails 製ウェブアプリで出会った話ですが、ちょっと原因に悩んだので書いておきます。

概要

Rails 製のウェブアプリのログを眺めていると、しばしば CSRF トークン検証エラーが記録されていました。ログインページでログインする際にデータを POST したときに、 CSRF トークン検証エラーが出たようです。
状況的に、外部から攻撃を受けているわけではなく、ユーザーもブラウザからアクセスしたようでした。

結局のところ、バグなどではなく、期待される挙動だったのですが、ちょっと悩んだので書いておきます。

再現手順

ブラウザ起動後の初回アクセスで、ウェブアプリのページを複数同時に開くと発生します。
「複数同時」というのがポイントです。

具体的には、以下の手順で発生します。

  1. 準備として Rails 製ウェブアプリで POST を使う複数のページを、二つブックマークに登録しておきます。
    例えば、「ログインページ」と「ユーザー登録ページ」などが該当するだろうと思います。
  2. 一旦ブラウザを閉じ、新たに開き直します。
  3. 「すべてのブックマークをタブで開く」などで、登録しておいた二つのページを同時に開きます。
  4. 開いた二つのページの少なくともどちらかでは、 POST 時に CSRF エラーが出ます。

原因や詳細

原因はセッション管理のされ方にあります。

Rails での CSRF 対応

Rails では、デフォルトで CSRF の対応がされています。
これは、セッション内に _csrf_token というキーで保存された値と、 POST 時に hidden field の authenticity_token で指定された値を比較することで、実現されています。
Ajax の場合には HTTP ヘッダ内に指定することもありますが、あまり関係ないので省略します。

要は、 CSRF トークンという、セッション毎にランダムに生成される値が、 POST で渡されてくる値と一致しているかを見ています。

セッションと Cookie

Rails では、セッションは URL Rewriting などではなく、デフォルトで Cookie を用いて実現されています。
また、 Cookie の有効期限はブラウザ終了までとなっています。

よって、デフォルトの設定では、セッションはブラウザを閉じたタイミングで破棄されます。つまり、ブラウザを開いた後の初回アクセス時に新規にセッションが生成されることになります。
この結果、 CSRF トークンは、ブラウザを開いた後の初回アクセス時に生成されます。
二回目以降のアクセスでは、セッション内に保存されている作成済みの CSRF トークンが使われます。

CSRF トークン検証エラーが起こる仕組み

以上のことから、複数のページを同時に開くと、以下の原理で CSRF トークン検証エラーが起こります。

  1. ブラウザを開きます。
  2. Rails 製ウェブアプリの複数のページを同時に開きます。
    仮に、ページ A, B とします。
  3. ページ A を GET する際に、新規にセッションが開かれ、それに伴い CSRF トークンが生成されます。
    このとき、ページ A 内の hidden field には、同じ CSRF トークンが設定されており、整合性がとれた状態になっています。
  4. これとほぼ同時に、ページ B が GET されます。
    ページ A の GET が完了してからであれば、ページ A で開かれたセッションが使われますが、ほぼ同時に開いた場合は、ページ A の GET は完了していないので、新規に別のセッションが開かれ、別の CSRF トークンが生成されます。
    ページ B 内の hidden field には、こちらの CSRF トークンが設定されることになります。
    この結果、ページ A の hidden field とページ B の hidden field には、別の CSRF トークンが authenticity_token として設定されることになります。
  5. この状態で、ページ A において POST をします。
    すると、セッションはページ B で開かれたものが使われます。
    これは、セッションが Cookie で管理されているためで、 Cookie は後から設定されたもので上書きされるためです。
  6. その結果、ページ A の hidden field の CSRF トークンと、セッション内の CSRF トークン (ページ B 由来のもの) は、異なるものとなり、 CSRF トークンの検証でエラーとなります。

最初は、どこかから CSRF 攻撃を受けているのかと思って、焦りました。
セキュリティ強度を下げずに回避するのは難しいかな。

2013年5月28日火曜日

Windows 上の Ruby 2.0 での sqlite3 の利用

Windows 上の Ruby 2.0 での sqlite3 の利用

最近は仕事が忙しすぎて、 mruby もブログも触る暇がないですが、久しぶりの備忘録です。
いくつか mruby などでリクエストをいただいていますが、なかなか対応できていなくて、本当にすみません。

MinGW 版 32bit Ruby 2.0 での sqlite3 の利用

2013年5月27日時点では、 sqlite3 の Windows 用 gem (version 1.3.7) には Ruby 2.0 向けのバイナリが入っていないようです。
そのため、 MinGW 版の 32bit Ruby 2.0 で Rails を使おうとすると、以下のようなエラーが出るようです。

私は RubyInstaller の 32bit 版を使わせていただいていますが、それに向けた sqlite3 のバイナリを置いておきます。
あくまで MinGW 版の 32bit Ruby 2.0 でしか確認していません。

準備とバイナリのダウンロード

下記のコマンドで、 sqlite3 をインストールしておきます。

このコマンドにより、 Ruby が C:/ruby にインストールされている場合は、 C:/ruby/lib/ruby/gems/2.0.0/gems/sqlite3-1.3.7-x86-mingw32 に gem がインストールされると思います。

続いて、 C:/ruby/lib/ruby/gems/2.0.0/gems/sqlite3-1.3.7-x86-mingw32/lib/sqlite3 の下に 2.0 ディレクトリを作成します。
おそらく 1.8, 1.9 というディレクトリは既にあるので、同列に 2.0 ディレクトリを作成します。

あらかじめ作成済みの sqlite3_native.so をダウンロードし、この 2.0 ディレクトリの下に置きます。

後は、普通に使えば大丈夫です。

以下に、作り方などの詳細を書いておきます。

Ruby 2.0 での sqlite3

Ruby から sqlite3 を使うには、 rubygems の sqlite3 を使うのが手軽です。
特に、 Ruby on Rails などでは、デフォルトでこの gem を使うようになっています。

しかし、 2013年5月27日時点では、 sqlite3 の Windows 用 gem には Ruby 2.0 向けのバイナリが入っていないようです。

ここでは、 Windows 上の 32bit 版 Ruby 2.0 で sqlite3 gem を使う方法について、書いておきます。
ただし、いずれは gem 側で解消されるだろうと思いますし、この方法はあくまで単なるパッチです。

Windows 上での Ruby 2.0 での sqlite3 gem の準備

いくつか方法はありますが、私は RubyInstaller を使わせていただいています。

Ruby や DevKit のダウンロードと展開

Ruby 2.0 のダウンロード
RubyInstaller のダウンロードページから、 Ruby 2.0.0-p*** をダウンロードし、好きなところに展開します。
ここでは、 C:/ruby と仮定します。
DevKit のダウンロード
RubyInstaller のダウンロードページから、 DevKit-mingw64-32-4.7.2-20130224-1151-sfx.exe をダウンロードし、好きなところに展開します。
ここでは、 C:/devkit と仮定します。
パスなどの初期設定
コマンドプロンプトを開き、以下のように ruby.exe へのパスを通しておきます。 また、続けて DevKit にもパスを通しておきます。 C:\devkit に移動し、以下のコマンドを実行します。 続いて、 DevKit の初期設定をしておきます。下記のような config.yml を作成します。 以下のコマンドを実行し、 DevKit の初期設定をします。

sqlite3 のダウンロードと展開

SQLite3 のソースコードをダウンロードページからダウンロードしておきます。
Source Code の欄にある sqlite-amalgamation-***.zip がビルドしやすくて便利です。

これを C:\sqlite3 などに展開し、このディレクトリに移動し、以下のコマンドで libsqlite3.a を作成します。

gem のインストール

以下のコマンドで、 sqlite3 gem をインストールします。

2012年6月3日日曜日

Add Subversion Links の Redmine 2.0 対応

Add Subversion Links プラグインを Redmine 2.0 対応したので、そのときのメモです。

Add Subversion Links プラグイン

Add Subversion Links プラグインは、Redmine のプラグインです。

このプラグインを入れておくと、 Redmine のチケット画面やリポジトリ画面に、 Subversion リポジトリ本体へのリンクが勝手に追加されます。

詳しくは GitHub の説明ページを見てください。

Redmine 2.0 対応

Redmine 2.0 では、ベースとなる Rails のバージョンが 3.2 に上がりました。
その結果、多くのプラグインの互換性がなくなったので、 Redmine 2.0 にバージョンアップしていない人も多いと思います。

Add Subversion Links の場合も、動かなくなっていました。

一つめの原因は、 Rails 3 で導入された html_safe に関するものでした。
Rails 3 では、「エスケープされていない String」と「エスケープ済みの SafeBuffer」を区別して扱うことで、エスケープ漏れや、多重のエスケープを回避しています。

Add Subversion Links では、 Subversion リポジトリへのリンクを生成する部分で、一部、エスケープしていない文字が入っており、結果として正しい表示になっていませんでした。
(エスケープしていない文字は、単なる空白だったので、最初は何が起こったかよく分かりませんでした。)

もう一つは、 image_path メソッドが削除されたことでした。

原因はよく調べていませんが、プラグインが持つ image assets の場所を返すメソッド image_path がなくなっていました。

Add Subversion Links では、一部、 JavaScript で動的にリンクを生成している箇所があり、そのリンク画像生成のために image_path を使っていたので、エラーになっていました。
結局、こちらは image_tag というタグ自体を生成するメソッドは残っていたので、こちらを使うように処理を変更することで対応しました。

Add Subversion Links プラグインの仕組み

Redmine 2.0 対応とは関係ないですが、このプラグインの仕組みを少しだけ書いておきます。

Redmine プラグインの作り方は、他にたくさんの優れたページがあるので、ここでは書きません。

ここでは、 Add Subversion Links プラグインを作る上での話について少し書きます。

メソッドの上書き

プラグインを入れると、下記のような Subversion のアイコンが現れ、リポジトリ本体へのリンクが追加されます。

example

これは、 application.js 内の link_to_revision メソッドを上書きして、アイコンを追加してやるだけで簡単に実現できました。

JavaScript による書き換え

面倒だったのは、下記の Name 列のような、個々のファイルへのリンクでした。

example2

こちらの方は、 link_to_revision のような、専用のメソッドがあるわけではないので、プラグインからメソッドを上書きして機能を変更するということが、簡単にはできなさそうでした。

というわけで、方針を変えて、 JavaScript で load 時に動的に追加してやることにしました。

しかし、この Name 列の内容は、 Ajax で動的に追加されるものであるため、 load 時にリンクを作るだけでは、後から Ajax で追加された要素に対してリンクを生成することができませんでした。

そこで、面倒だったのですが、 Ajax で Name 列の要素が生成されたときに呼ばれるコールバック関数 (prototype.js の Ajax.Updater の onSuccess 属性に渡す関数) をフックしてやり、そのフック関数の中で Subversion リポジトリ本体へのリンクを生成してやりました。

コードとしては以下のようになっており、 onSuccess で呼ばれる scmEntryLoaded を Rails の alias_method_chain のように上書きしてやることで、リンクを生成する処理を追加しています。
また、 Ajax.Updater による要素の追加は、 onSuccess の直後に行われるので、実際にリンクを追加する addRepositoryLinkInRepositoryPage は、 setTimeout を使って遅らせています。

これで、めでたく Redmine 2.0 に対応できました。
JavaScript の部分以外はテストも書いたので、少しは (面倒であまり楽しくない) メンテナンスも楽になるはずです。

2011年12月12日月曜日

Redmine Plugin で ApplicationHelper を拡張する際の注意

Redmine Plugin で ApplicationHelper を拡張する際の注意

Windows PC の調子が悪いので、NPAPI プラグインの作り方は少し延期です。

今回は、Redmine 1.3.0もリリースされたので、 Redmine Plugin で ApplicationHelper を拡張する際に悩んだ問題について書きます。

Redmine プラグインで、既存のクラスやモジュールを拡張する

Redmine プラグインを作成し、 ApplicationHelper などの既存のクラスやモジュールを拡張する場合、基本的には以下のようにします。

  1. プラグインのlib以下にredmine_sample_plugin_application_helper_patch.rbのようなファイルを作成し、以下のように記述します。
  2. init.rb内部で、redmine_sample_plugin_application_helper_patch.rbをrequireします。

上に書いたように、一般的にはApplicationHelperを再定義せずに、追加したり置換したりしたいメソッドを別モジュールにまとめた上で、これをインクルードさせることが多いです。

View のフック関数からの呼び出し

Redmine のプラグインへのフレームワークとして、View の決められた場所に、プラグインのコードを埋め込む、という仕組みがあります。

ここでは説明しませんが、知りたい方はRedmine自体に手を入れずに見た目を変更する方法などをご覧ください。

いずれにせよ、Redmine::Hook::ViewListenerを継承したクラスを作成し、その中にあらかじめ定められた名前のフックメソッドを定義することで、View に任意のコードを追加できます。

View のフック関数から、追加したメソッドの呼び出し

上のSamplePluginApplicationHelperPatchのように定義した場合、新規に定義したメソッド (上の例ではsample_instance_method) は、View のフック関数から呼ぶことができません。

ApplicationHelper 内のメソッドは、すべてのコントローラーや View から呼べるはずなのですが、呼ぶことができないのです。

呼び出せない理由

ApplicationHelperは、Redmine::Hook::ViewListener内で適切にincludeされています。
また、上で定義したサンプルでは、確かにSamplePluginApplicationHelperPatch::InstanceMethodがApplicationHelperにインクルードされています。
そのため、間接的にSamplePluginApplicationHelperPatch::InstanceMethodはRedmine::Hook::ViewListenerからインクルードされているように思えます。

しかし、実際にはそうなりません。

Redmine::Hook::ViewListenerがApplicationHelperをインクルードするのは、プラグインの各ファイルがロードされるよりも早いタイミングで行われます。
そのため、Redmine::Hook::ViewListenerがApplicationHelperをインクルードする頃には、まだSamplePluginApplicationHelperPatch::InstanceMethodはApplicationHelperにインクルードされていません。

Ruby では、includeしたタイミングで継承関係が決定します。

例を挙げると、以下のようになります。

上の例で、B.ancestorsにCが含まれてもよさそうですが、含まれません。

これは奇妙なように思いますが、 Ruby の仕様であり、どうしようもありません。

余談ですが、 Ruby 作者のまつもとさんも、これは奇妙に思っているらしく、Matzにっきの Traits の項目に、今後mixを追加しようと思っている旨が書かれています。

そのため、もともとの Redmine の ViewListener に関しても同様に、Redmine::Hook::ViewListenerがApplicationHelperをインクルードするタイミングでは、まだSamplePluginApplicationHelperPatch::InstanceMethodがApplicationHelperにインクルードされていないので、結果としてRedmine::Hook::ViewListenerの ancestors にはSamplePluginApplicationHelperPatch::InstanceMethodが含まれないことになります。

回避策

これは、例えば以下のようにすれば回避することができます。

このようにすることで、sample_instance_method自体はApplicationHelperに直接追加されるので、Redmine::Hook::ViewListenerからでも呼び出すことが可能になります。

もちろん、base.class_eval doの内部で、直接sample_instance_methodを定義しても呼び出すことができますが、このあたりはコードの見易さの問題で、好みの分かれるところだろうと思います。

2011年11月15日火曜日

requireとrequire_dependencyとRuby 1.8.7とRuby 1.9.2

requireとrequire_dependencyと Ruby 1.8.7 と Ruby 1.9.2

先日、Redmineの Version 1.2.2 もリリースされたので、プラグインを書くときに気になった、requireとrequire_dependencyの違いおよびRuby のバージョンによるrequireの挙動の違いについて書こうと思います。

とは言っても、よく書かれている「カレントディレクトリがロードパスに含まれなくなった」ということではありません。

なお、これは Rails 2.3.11, 3.1.0 を見て書いています。また、Redmine は Version 1.2.2 を見ながら書いています。

requireとrequire_dependency

Rails のコードを見たことがある人は分かると思いますが、Rails のコードではrequireの代わりにrequire_dependencyが使われていることに気付くと思います。

それぞれ簡単にまとめると以下のようになります。

require
Ruby の組込み関数。正確には、Kernelモジュールのメソッド。
引数で渡された .rb ファイルなどを読み込む。ただし、既に読み込み済みのファイルは読み込まない。
読み込まれたモジュールは$", $LOADED_FEATURESに格納されて管理される。
require_dependency
Rails が定義した独自のメソッド。activesupport/lib/active_support/dependencies.rbで定義されている。
引数で渡された .rb ファイルなどを読み込む。ただし、既に読み込み済みのファイルは読み込まない。development モードで実行している場合はloadを使って読み込まれる。production モードで実行している場合は、requireを使って読み込まれる。
これは、development モードで実行している場合は、動的にファイルを何度も読み込む必要があるため。

Redmine プラグインで注意すること

結論から書くと、Redmine プラグインの中では、Redmine のモデル、ビュー、コントローラー、ヘルパーを読み込むには、require_dependencyを使うべしということです。

ここで、requireを使うと失敗します。

上の説明で書いたように、production モードで実行するならどちらでも結局requireが呼ばれるので変わらない、と思うかもしれませんが、そうではありません。

以下で順に説明します。

Ruby 1.8.7 と Ruby 1.9.2 でのrequireの挙動の違い

まず、requireの挙動をもう少し詳しく見てみます。

Ruby 1.8.7 でのrequireの挙動

main.rbから、required.rbを、下記のようにrequireする場合を考えます。

ここでmain.rbを実行すると、出力は下記のようになり、二回目のrequireではrequired.rbがロードされていないことが分かります。

これは当然の挙動だと思います。

次に、以下のように、相対パスや絶対パスを指定してrequired.rbを読み込んでみます。

出力は以下のようになります。

このように、予想に反して (かどうかは分かりませんが) 三回とも読み込まれています。

Ruby 1.9.2 でのrequireの挙動

一方で、同じコードを Ruby 1.9.2 で実行してみます。

ただし、Ruby 1.9.2 では、カレントディレクトリがロードパスに含まれなくなっているので、先頭で追加してやった状態で実行してみます。

出力は以下のようになります。

今回は、一度しか読み込まれませんでした。

このように、Ruby のバージョンによってrequireの挙動は微妙に異なります。

Ruby 自体のソースを見れば分かりますが、「既に読み込まれたか」を表すリストである$", $LOADED_FEATURESに記憶する方法が、Ruby 1.8.7 と Ruby 1.9.2 では異なるようです。

Ruby 1.8.7 では指定されたファイルをそのままの形で$LOADED_FEATURESに覚えておきます。 そのため、require("required")やrequire("./required")など、指定方法が異なると、複数回ロードされることとなります。

一方で、Ruby 1.9.2 では、常に絶対パスに直して覚えるようになっています。 その結果、実際に同じファイルであれば、指定方法が異なっても複数回ロードされることはありません。

Redmine プラグインでrequire_dependencyを使わないと正しく動作しない理由

以上を踏まえて、なぜ production モードであっても Redmine プラグインでrequire_dependencyを使わないとうまく動作しないのか、について書きます。

Redmine は、現在も Ruby 1.8.7 で動作しています。

そのため、あるファイルをrequireする際に、絶対パスで指定した箇所とファイル名だけを指定した箇所がある場合、そのファイルは二重にロードされることになります。

一方で、require_dependencyでは、コードを追うと分かりますが、最終的に引数のファイル名を絶対パスに展開してからrequireもしくはloadします。

ここで、以下のように書いてしまった場合を考えてみます。

この例では、ApplicationHelperモジュールのformat_activity_titleメソッドを上書きしています。

しかし、このファイルとは別のファイルでrequire_dependency("application_helper")と書かれていると、そちらではapplication_helperが絶対パスに展開されてから、requireに渡されることになります。

そのため、ファイル名だけを指定してrequire("application_helper")としたときとは別のファイルとして扱われてしまい、別途、application_helper.rbがロードされることになります。

その結果、せっかく上書きしたメソッドが再度上書きされてしまい、変更が反映されなかったように見えてしまいます。

というわけで、素直にrequire_dependencyを使うようにするのがよいと思います。

2011年11月7日月曜日

Redmine プラグイン その2

Redmine プラグイン その2

昨日の続きです。

Redmine プラグインの雛形の作成

Redmine のプラグインを作成するには、以下のようなコマンドを実行します。 Redmine 1.2.1 では、まだ Rails 2.3.X 系統なので、ruby script/generateでプラグインを作成します。

このコマンドにより、vendor/pluginsディレクトリの下にredmine_sample_pluginというディレクトリが作られ、プラグインの雛形が生成されます。

プラグインの仕組み

今回は、簡単のためにデータベースへの保存などは行わず、表示方法をカスタマイズしたり、機能を追加したりする場合について書きます。

init.rbからrequireする

この雛形に含まれるファイルで、最重要なものはinit.rbです。

このファイルは、Redmine が起動する際に自動で読み込まれます。

また、読み込まれる順序は、Redmine の Core ファイルが読み込まれた後です。

前回のオープンクラスの部分で書きましたが、この順序で読み込まれることが保証されているので、init.rbの中で Redmine 関係のクラス (例えばIssueクラス) を再定義することにより、自由にメソッドを追加、削除、上書きすることができます。

例えば、以下のように書いたファイルをlib以下に置き、init.rbからrequireすると、Issueクラスにopen?メソッドを追加することができます。 (ただし、後で書くように、普通はもう少し整理された定型の書き方があります。)

このように書いたファイルをinit.rbからrequireすることで、新たにIssueクラスに新しいメソッドを追加することができます。

重要なポイントは、Issueクラスという既存のクラスへの変更が、既存のファイルであるapp/models/issue.rbをまったく変更することなく可能となっている点です。

これは、Issueクラスのメソッドの全体が分かりにくいという大きな欠点もできますが、プラグインを配布する際に、特定のディレクトリにファイルを展開するだけでよいという、モジュールの可搬性という点において大きな利点を生むことになります。

実際のファイルは、以下のようにして、インスタンスメソッドやクラスメソッドを分離して記述することが多いです。

app以下で上書きする

これとは別に、appディレクトリ以下に直接ファイルを置くことによって、既定の動作を上書きして変更することもできます。

例えば、app/views/issues/_edit.rhtmlの内容を少し変更したものをvendor/plugins/redmine_sample_plugin/app/views/issues/_edit.rhtmlに置くことにより、もともとの_edit.rhtmlではなく、redmine_sample_plugin以下に置かれた_edit.rhtmlを使わせることができます。

これは、Rails がファイルを検索する順序として、各プラグインのapp以下を探し、見つからなければルートディレクトリ直下のappディレクトリを探すとなっているためです。

そのため、同名のファイルをプラグインのapp以下に同じディレクトリ構成で置いてやることにより、自由にデフォルトのファイルを置き換えることができます。

簡単に表示やデフォルトの挙動をカスタマイズする程度であれば、このあたりを理解するだけで比較的自由に変更することができると思います。

一方で、あまりに自由度が高いので、これをやりすぎると、全体の見通しが悪くなったり、他のプラグインとの関係で思わぬ副作用が出たりするかもしれません。

Rails は、そもそも組込みのクラスにもいろいろなメソッドを (勝手に) 追加しています。こういう部分は、きちんとした設計のもとに行えば、非常に使い勝手が上がりますが、下手に追加すると、混乱を招くだけです。

Rails では、きちんとした設計のもとに行うことで、利便性を実現しています。

それと同じように、プラグインに対してもユーザーが自由にカスタマイズできるようにしておき、「自己責任でプログラマが自由にしてね」という余地をあえて残すような設計にしているのかもしれません。

2011年11月6日日曜日

Redmine プラグイン

Redmine プラグイン

以前に Redmine プラグインの練習として作ってみたWiki GChart Formula Pluginの紹介をかねて、Redmine プラグインについて書きます。

オープンクラス

Redmine, Rails に限った話ではありませんが、Ruby のクラスは、オープンです。

Redmine や Rails のプラグインは、このオープンクラスの仕組みをうまく使っているので、今回はその説明を書くことにします。

「クラスがオープンである」というのは、例えば、一度クラスを定義した後からでも、自由にメソッドの追加や上書きなどができるということです。

例として、以下のような Ruby のプログラムを考えます。Testクラスが二回定義されていることに注意してください。

このスクリプトを実行すると以下のように出力されます。

old
new
ok

この例では、(1) で定義したメソッドprintが (4) で再定義され上書きされます。また、(5) で新しいメソッドprint2が新規に追加されています。

C# の部分クラス定義との違い

これは、C# の部分クラス定義の機能を使って、複数ファイルでクラスの各メソッドを定義できることと似た部分があります。

しかし、Ruby ではクラスの定義自体も通常のメソッド呼び出しなどと同じように実行時に行われる点が大きく違います。

C# などでは、定義自体が分割されていようがなかろうが、クラスの定義自体はコンパイル時に解釈され、実行時にクラスのメソッドが途中で動的に追加されたり置き換わったりすることはありません。

そのため、C# でもともと提供されている既存のクラスに対してメソッドを追加するようなことはできません。 また、(1), (4) のように同名のメソッドを定義することもできません。 というか、後から定義した方で上書きできたとしても、実行時にはその片方の定義しか使われないので、意味がありません。

一方で、Ruby ではそれらが可能です。

そのため、上に書いたコードは以下のように動きます。

  • (2) の段階では、まだ (4) の再定義がされていないので、(1) で定義された通りoldが出力されます。
  • (3) の段階では、まだ (5) の定義がされていないので、print2などというメソッドは存在せず、エラーになります。
  • (6) の段階では、(4) の再定義の後なので、newが出力されます。(1) で定義されていた、古いprintメソッドを呼ぶ方法はありません。
  • (7) の段階では、(5) の定義の後なので、okが出力されます。

このように、クラス定義も実行時に行われ、かつクラスがオープンであるために、後から既存クラスの挙動を変更することが非常に柔軟に行えます。

これらの挙動を利用することで、Redmine や Rails では、プラグイン側から Redmine や Rails のクラスを上書きすることにより、自由なカスタマイズをすることができます。

続く。