読者です 読者をやめる 読者になる 読者になる

特異クラスを使ってRailsのmodelを動的に作成する

Railsのmodelを動的に定義したくて悩んでたらTwitterで @antimon2 さんがうまい方法を教えてくれました。

# このmodelを継承したクラスを作成すれば
# validationを行ったり、viewでform_forが使える

class SimpleModel
  include ActiveModel::Validations
  include ActiveModel::Conversion
  extend ActiveModel::Naming

  def initialize(attributes = {})
    attributes.each do |name, value|
      send("#{name}=", value)
    end
  end

  def persisted?
    false
  end
end
# controllerの一部
# SimpleModelの特異クラスを定義
# headが1に対してdetailsが多
# detailのforce_inputがtrueなら必須入力とする

  def create_model_instance(head)
    model = SimpleModel.new
    detail_class = class << model;self;end
    detail_class.class_eval do
      head.details.each do |d|
        column = "detail#{d.id.to_s}".to_sym
        attr_accessor column
        if d.force_input
          validates column, :presence => true
        end
      end
    end
    model
  end   

初め

detail_class = class << model;self;end

の部分がよく分からなくて悩んでたんだけど次のエントリを見て理解できた。
http://d.hatena.ne.jp/unageanu/20080729
SimpleModelの特異クラスを取り出しているらしい。
んでは、一旦detail_classを作成してdetail_class.class_evalするより直接次のように書いたほうが
シンプルじゃね?と試してみたら...

  def create_model_instance(head)
    model = SimpleModel.new
    class << model
      head.details.each do |d|
        column = "detail#{d.id.to_s}".to_sym
        attr_accessor column
        if d.force_input
          validates column, :presence => true
        end
      end
    end
    model
  end
ruby-1.9.2-p0 > x = create_model_instance(Head.first)
NameError: undefined local variable or method `head' for #<Class:#<SimpleModel:0x00000101f9ef78>>

この位置ではheadは取得できないようです。スコープ範囲外ってことか。なるほどネ...
特異メソッドについては理解してたつもりだったんだけど、今回特異クラスについて学習することができました。
特異クラスを使えばattr_accessorやvalidatesなどのクラスに対して行う操作が特定インスタンスに関連する
クラスにのみ行うことができます。勉強になりました。