RailsでMySQLのENUM型を使う方法

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でやる方法

RailsENUM型を使おうと思ったら以下のように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環境ではなぜ正常に扱われるかが分からない。
もうちょっと調べて調べたらアップします!
以降は、railsENUM型を使う方法と、その際の注意点を書きます。

enum-columnを使う

railsENUM型を使うには「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?にあるので、そこをオーバーライドします。

  • enum-column/lib/enum/active_record_helper.rb
  # 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