2011年7月13日水曜日

Ruby Mix-inのタイミング(覚書)

Railsのプラグインを作成中に遭遇した問題を覚書にしておきます。

t.timestampsのような、任意の複数のカラムをまとめて作成する
処理を書いてしていました。

テーブルの新規作成時は
ActiveRecord::ConnectionAdapters::TableDefinition クラスに
独自のメソッドを追加すれば、db:migrate するときにカラムが追加されます。
これは問題ありません。

テーブル定義を変更する場合、例えばtimestampsは
ActiveRecord::ConnectionAdapters::SchemaStatements モジュールに実装されている
add_timestamps メソッドを呼び出しています。

ただし、この時のレシーバのクラスは
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
でした。(DBはPostgreSQLを使用しているので)

これと同じ処理(メソッド)を、上記のSchemaStatementsに追加したのですが、
メソッドがない・・・method_missing とおこられてしまった・・・

継承関係は下記の通り
>> ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.ancestors
=> [ActiveRecord::ConnectionAdapters::PostgreSQLAdapter, ActiveRecord::ConnectionAdapters::AbstractAdapter, ActiveSupport::Callbacks, ActiveRecord::ConnectionAdapters::QueryCache, ActiveRecord::ConnectionAdapters::DatabaseLimits, ActiveRecord::ConnectionAdapters::Quoting, ActiveRecord::ConnectionAdapters::DatabaseStatements, ActiveRecord::ConnectionAdapters::SchemaStatements, Object, JSON::Ext::Generator::GeneratorMethods::Object, ActiveSupport::Dependencies::Loadable, Base64::Deprecated, Base64, Kernel]



試しに以下のようなプログラムを書いてみると・・・

#モジュールを2つ用意
>> module A
>> def a
>>  p "a"
>> end
>> end
=> nil
>> module B
>> def b
>>  p "b"
>> end
>> end
=> nil

#クラスを1つ用意 モジュールAをインクルード
>> class C
>> include A
>> end

>> c = C.new
=> #
>> c.a
"a"
=> nil
>> c.b
NoMethodError: undefined method `b' for #
 from (irb):39

#ここまでは特に問題なし

# モジュールAにモジュールBをインクルード
>> A.class_eval do
?> include B
>> end
=> A
>> A.instance_methods
=> ["b", "a"]

#Aのインスタンスメソッドに b が追加されました。


>> C.instance_methods.grep /^a/
=> ["a", "acts_like?", "as_json"]
>> C.instance_methods.grep /^b/
=> ["blank?", "breakpoint", "b64encode"]
#ところが、既にモジュールAをMix-inしてしまっていた クラスCには
メソッドb がありません・・・

>> class C
>> include A
>> end
=> C
>> C.instance_methods.grep /^b/
=> ["b", "blank?", "breakpoint", "b64encode"]

#あらためてモジュールAをMix-inすると、メソッドbが追加されました。
#Mix-inはインクルードするタイミングによって、期待したメソッドが追加されない
可能性があるようです。

ちなみに、Rubyのバージョンは 1.8.7 です。
・・・さて、どうしたものか・・・