DBのカラムで、ステータスの様なカラムを扱いたい場合、ENUM型を使いたいことが多いです。
- 例えばこんな感じ
CREATE TABLE blog_comments( id INTEGER NOT NULL AUTO_INCREMENT, blog_id INTEGER NOT NULL, commenter VARCHAR(40) NOT NULL, content TEXT NOT NULL, status ENUM('approved','nonapproved') NOT NULL, FOREIGN KEY (blog_id) REFERENCES blogs(id), PRIMARY KEY (id) );
Railsでやる方法
RailsでENUM型を使おうと思ったら以下のようにMigrationファイルを定義すると思います。
- create_comments
class CreateComments < ActiveRecord::Migration def self.up create_table "comments", :force => true do |t| t.column :blog_id, :integer, :null => false t.column :commenter, :string, :limit => 40, :null => false t.column :content, :text, :null => false end execute <<-SQL ALTER TABLE `comments` ADD status ENUM('approved', 'nonapproved') NOT NULL; SQL end def self.down drop_table "comments" end end
実際にこのファイルを作成し、「rake db:migrate」をしてやるとテーブルがきちんと作成されます。
mysql> show fields from comments; +---------+--------------------------------+------+-----+----------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+--------------------------------+------+-----+----------+----------------+ | id | int(11) | | PRI | NULL | auto_increment | | blog_id | int(11) | | | 0 | | | content | text | | | | | | status | enum('approved','nonapproved') | | | approved | | +---------+--------------------------------+------+-----+----------+----------------+ 4 rows in set (0.00 sec)
きちんと定義されています。しかし、Railsレベルでは、テスト環境で問題が発生します。
ENUM型を使う際の問題点
以下、script/consoleで実際に何が起こるのか見てみます。
- 開発環境の場合
Loading development environment. >> c = Comment.new(:blog_id => 1, :content => "hoge", :status => "approved") => #<Comment:0x471d26c @new_record=true, @attributes={"status"=>"approved", "blo g_id"=>1, "content"=>"hoge"}> >> c.save => true >> c.reload => #<Comment:0x471d26c @new_record=false, @attributes={"status"=>"approved", "id "=>"4", "blog_id"=>"1", "content"=>"hoge"}, @errors=#<ActiveRecord::Errors:0x471 89ec @errors={}, @base=#<Comment:0x471d26c ...>>>
- テスト環境の場合
Loading test environment. >> Comment.new(:blog_id => 1, :content => "hoge", :status => "approved") => #<Comment:0x4782a54 @new_record=true, @attributes={"status"=>"approved", "blo g_id"=>1, "content"=>"hoge"}> >> c = Comment.new(:blog_id => 1, :content => "hoge", :status => "approved") => #<Comment:0x4737d38 @new_record=true, @attributes={"status"=>"approved", "blo g_id"=>1, "content"=>"hoge"}> >> c.save => true >> c.reload => #<Comment:0x4737d38 @new_record=false, @attributes={"status"=>"", "id"=>"3", "blog_id"=>"1", "content"=>"hoge"}, @errors=#<ActiveRecord::Errors:0x4735f60 @er rors={}, @base=#<Comment:0x4737d38 ...>>>
テスト環境だど、保存後にはstatusの値が空になってしまっています。
なんでだー!!!この理由が分からない><
どっちも空になるなら話は分かる。実際に、schema.rbを見ると、ENUM型として指定した部分は、
t.column "status", :string, :limit => 0, :null => false
となってしまう。limitが0って、そりゃ空になりますよね。これはrailsの変換のためだと思うのですが、development環境ではなぜ正常に扱われるかが分からない。
もうちょっと調べて調べたらアップします!
以降は、railsでENUM型を使う方法と、その際の注意点を書きます。
enum-columnを使う
railsでENUM型を使うには「enum-column」というプラグインが使えます。これを使うと、先ほどのMigrationファイルの「execute」の部分が次の様に書けるようになります。
- create_comments
t.column :status, :enum, :limit => [:approved, :nonapproved], :null => false
schema.rbでもきちんとENUM型として解釈されています。
t.column "status", :enum, :limit => [:approved, :nonapproved]
rubyに型変換される際にはSymbolとして扱われます。newするときとかは文字列でもおk。
c = Comment.new(:status => "approved") c.status #=> :approved
詳細については、以下の記事が参考になります。
enum-columnを使用する際の問題点
チェックボックスを使用する際に注意が必要。例えば、次のようなフォームを書いたとします。
<% form_tag :action => "update",:comment => @comment do %> ■コメントを承認する <%= check_box :comment, :status, {}, :approved, :nonapproved %><br /> <% end %>
このフォームは正常に機能しません。値は正常にPostすることができますが、checkedの部分(後ろ2つの引数で指示している)が機能しないのです。
この状態だと、常にapprovedになります。
解決方法
問題はデフォルトのActionView::Helpers::FormHelper.check_box_checked?にあるので、そこをオーバーライドします。
# Gets the list of values for the column. def enum_values object.send("column_for_attribute", @method_name).values end #以下を追加 class << self def check_box_checked?(value, checked_value) case value when TrueClass, FalseClass value when NilClass false when Integer value != 0 when String, Symbol #valueがSymbolの場合もここで扱う value.to_s == checked_value.to_s #文字列に変換して比較 else value.to_i != 0 end end end end end end