2011-12-19
■[Ruby on Rails] テーブルに存在しない属性は型変換が行われない
Rails でデータベースにカラムが存在しない属性を扱いたい場合、通常は attr_accessor で属性を定義すると思います。しかし、attr_accessor で定義した属性は、テーブルにカラムが存在する属性のように、代入時の型変換が行われません。
例えば、以下のような会員クラスに利用規約の同意フラグを保持する属性を attr_accessor :accepted で定義したとします。
# 会員. class User < ActiveRecord::Base # 利用規約の同意フラグ. attr_accessor :accepted end
会員登録フォームの View ファイルでは、利用規約に同意する場合はチェックボックスにチェックするものとします。
<%= form_for(@user) do |form| %> ... 利用規約に同意しますか? <%= form.check_box(:accepted) %>はい ... <% end %>
利用規約に同意されなかった場合はエラーとしたいのですが、会員登録を行うコントローラで以下のように記述しても正しく動作しません(常に同意したものとして処理されてしまいます)。
def create @user = User.new(params[:user]) unless @user.accepted # 利用規約に同意していないのでエラー end end
原因は、accepted は attr_accessor で定義されているためです。利用規約のチェックボックスがチェックされなかった場合にクエストパラメータとして '0' が送られてくるのですが、accepted には文字列としての '0' がそのまま保持されます。Ruby では '0' も真として扱われるため、チェックしたかしないかによらず unless @user.accepted の内部には入りません。本当は true/false に型変換されると嬉しいのですが。
■[Ruby on Rails] テーブルに存在しない属性でも型変換を行う方法
テーブルに存在しない属性でも型変換を行う方法を考えます。
Rails では、カラムの情報は ActiveRecord::ConnectionAdapters::Column クラスが保持しています。そして、代入時の型変換は ActiveRecord::ConnectionAdapters::Column#type_cast メソッドで行われます。
# ActiveRecord::ConnectionAdapters::Column.new(name, default, sql_type = nil, null = true) column = ActiveRecord::ConnectionAdapters::Column.new(:accepted, false, :boolean) column.type_cast('1') # => true column.type_cast('0') # => false
そこで、attr_accessor の変わりに、代入時に ActiveRecord::ConnectionAdapters::Column#type_cast で型変換を行うメソッドを定義します。
class ActiveRecord::Base def self.column(name, type, default=nil) ActiveRecord::ConnectionAdapters::Column.new(name, default, type).tap do |column| define_method("#{name}=") do |value| instance_variable_set("@#{name}", column.type_cast(value)) end define_method(name) do unless instance_variable_defined?("@#{name}") instance_variable_set("@#{name}", column.default) end instance_variable_get("@#{name}") end end end end
そして、上記の column メソッドでカラムがテーブルに存在しない属性を定義します。
# 会員. class User < ActiveRecord::Base # 利用規約の同意フラグ. column :accepted, :boolan, false end
これで代入時に型変換が行われるようになりました。
user = User.new(:accepted => '1') user.accepted # => true user = User.new(:accepted => '0') user.accepted # => false