RailsのenumとSTIを組み合わせてみたらハマった件

# まずはenumの話
## enumって？
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/enum.rb

列挙体がRails＋ActiveRecordで利用できるよ（4.1.8以降）
元javaエンジニア的にはちょっと嬉しい

----

### その他のライブラリ
標準ではないライブラリたち。今回は扱わない。

https://github.com/AgilionApps/classy_enum
クラスベースなもの

https://github.com/brainspec/enumerize
i18nに対応しているもの

----

## どう使うの？

----

### 下準備
```ruby:user.rb
class User < ActiveRecord::Base
  enum role: { admin: 10, guest: 20 }
end
```

```ruby:migration
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.integer :role
    end
  end
end
```

----

### 値の取り扱い
いろいろなアクセス方法が用意されているので、場面に合わせて使い分けよう。

----

#### set系

----

##### 値を設定する方法
基本的にはシンボルまたは文字列で設定する。

```ruby
user.role = :guest # => "guest"
user.role = 'guest' # => "guest"
```

添字でも設定可能だけど、あまり使いどころはなさそう？

```ruby
user.role = 20 # => 20
```

----

##### 値を変更する方法
このパターンだと即時Updateが発生する。

```ruby
user.guest!
#   (0.5ms)  BEGIN
#  SQL (0.5ms)  UPDATE `users` SET `role` = 20 WHERE `users`.`id` = 1
#   (12.2ms)  COMMIT
#=> true
```

----

#### get系

----

設定値を文字列で取得

```ruby
user.role # => "guest"
```

指定の要素かどうかを判定する

```ruby
user.guest? # => true
user.admin? # => false
```

----

#### 全種類取得
select_box作る時などに使える。

```ruby
User.roles # => {"admin"=>10, "guest"=>20}
```

----

#### DBアクセス
保存される値を取り出して、それで検索
integerで保存されていれば範囲検索もできる

```ruby
User.where(role: User.roles[:admin])
# [#<User:0x00000000000000
#   id: 1,
#   role: 10>]
```

```ruby
User.where(role: User.roles[:admin]..User.roles[:guest])
```

----

### 具体例 : form + select_box

----

#### view

```haml:_form.html.haml
= form_for @user do |f|
  = f.select :roles, User.roles.keys.to_a
```
こんな感じに表示される
<img width="300px" alt="select.png" src="https://qiita-image-store.s3.amazonaws.com/0/21325/48d95ebe-3c0e-b593-a81f-5c3c9a2e9bbd.png">

----

#### view + i18n
日本語化も標準にある機能だけでできるよ。

```haml:_form.html.haml
= form_for @user do |f|
  - select_options = User.roles.keys.map do |role|
    - [t("roles.#{role}"), role]
  = f.select :roles, select_options
```

```yml:ja.yml
ja:
  roles:
    admin: 管理者
    guest: ゲスト
```

<img width="300px" alt="select_i18n.png" src="https://qiita-image-store.s3.amazonaws.com/0/21325/da0455ed-f728-caa1-2da0-edc8544bce02.png">

----

#### controller

特に処理しなくてassignできる！

```ruby:users_controller.rb
def update
  # params = { "user" => { "role" => "admin" } ... }
  @user.update_attributes(user_params)
end
```

----

### Tips・注意点

----

デフォルト値はあったほうが運用にやさしいので設定しよう。

```ruby:user.rb
class User < ActiveRecord::Base
  enum role: {
    no_role: 0,
    admin:  10,
    guest:  20,
  }
end
```

```ruby:migration
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.integer :role, null: false, default: 0
    end
  end
end
```
----

#### DBに保存する値

文字列やbooleanをDBに保存できる
（当然migrationもそれに合わせよう）

```ruby:user.rb
class User < ActiveRecord::Base
  enum role: {
    admin: :admin,
    guest: :guest,
  }
end
```

```ruby:user.rb
class User < ActiveRecord::Base
  enum role: {
    admin: true,
    guest: false,
  }
end
```
----

#### Arrayで設定できる（推奨しない）

```ruby:user.rb
class User < ActiveRecord::Base
  enum role: [:admin, :guest]
end
```

Arrayの添字がDBに保存されるよ
なので、要素を追加する際、末尾に追加しないとずれるので止めたほうが無難

----

#### 要素の名前はかぶせない

怒られる。

```ruby:user.rb
class User < ActiveRecord::Base
  enum role:    { admin: 10, guest: 20 }
  enum subrole: { admin: 10, guest: 20 }
end
```

```ruby
User.new
# ArgumentError: You tried to define an enum named "subrole" on the model "User", but this will generate a instance method "admin?", which is already defined by another enum.
# from (略)/vendor/bundle/ruby/2.2.0/gems/activerecord-4.2.7.1/lib/active_record/enum.rb:187:in `detect_enum_conflict!'
```

----

#### 名前がかぶらないように

prefixをつけよう

```ruby:user.rb
class User < ActiveRecord::Base
  enum role:    { admin: 10, guest: 20 }
  enum subrole: { sub_admin: 10, sub_guest: 20 }
end
```

----

#### 名前がかぶらないように（Rails5）

prefix, suffixをつける機能がRails5以降にはあるらしい

```ruby:user.rb
class User < ActiveRecord::Base
  enum role:    { admin: 10, guest: 20 }
  enum subrole: { admin: 10, guest: 20 }, _prefix: :sub
end
```

----

# STIと組み合わせた話

----
## Model
クラスごとに違う値を取るように制限したい！

```ruby:user.rb
class User < ActiveRecord::Base
  enum role: { no_role: 0 }
end
```

```ruby:manager.rb
class User::Manager < User
  enum role: { ceo: 110, cto: 120, cfo: 130 }
end
```

```ruby:worker.rb
class User::Worker < User
  enum role: { engineer: 210, hr: 220, account: 230 }
end
```

----

##　View

こんな感じで1個だけ選択して、`User#type`と`User#role`を送信するフォームを作ったという前提

<img width="400px" alt="select_work_role.png" src="https://qiita-image-store.s3.amazonaws.com/0/21325/0b974526-bb77-b0eb-ca81-641c11189d37.png">

----
## Contorller
ここで問題が！

```ruby:users_controller.rb
def new
  User.new(user_params)
end

def update
  @user.update_attributes(user_params)
end

# params = { "user" => { "type" =>  "User::Worker", "role" => "engineer" } ... }
```

----
### new
の場合は特に問題なく成功。

----
### update
の場合、かつtypeの変更がある場合はエラーになる！

```rb
user.type
# => User::Worker

user.update_attributes({ type: "User::Manager", role: :cto })
# => ArgumentError: 'cto' is not a valid role
```

----
### 回避策（失敗）

先にSTIのクラスを変化させればいけるんじゃね？（ダメでした）

```rb
user.becomes User::Manager
user.role = :cto
# => ArgumentError: 'cto' is not a valid role
```

----
### enumの仕組みどうなってんの？

how : メタプログラミングでインスタンスメソッドを作成している
when: Modelに書いた`enum :foo { :bar }`が評価された時

```rb:active_record/enum.rb（抜粋）
module ActiveRecord
  module Enum
    def enum(definitions)
      klass = self
      definitions.each do |name, values|
        # statuses = { }
        enum_values = ActiveSupport::HashWithIndifferentAccess.new
        name        = name.to_sym

        # def self.statuses statuses end
        detect_enum_conflict!(name, name.to_s.pluralize, true)
        klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }

        _enum_methods_module.module_eval do
          # def status=(value) self[:status] = statuses[value] end
          klass.send(:detect_enum_conflict!, name, "#{name}=")
          define_method("#{name}=") { |value|
            if enum_values.has_key?(value) || value.blank?
              self[name] = enum_values[value]
            else
              raise ArgumentError, "'#{value}' is not a valid #{name}"
            end
          }
```
----

### becomesすると何が起こるの？

Modelのtypeの値を変更しているだけ
= `becomes`後もインスタンスは`User::Worker`のまま！

```rb
user.becomes User::Manager`
# => #<User::Worker:0x00000000000000 id: 1, type: "User::Manager", role: 10>]
```

----
### 回避策1

一旦保存してreloadしたら行けるはず！（できた）

```rb
user.becomes User::Manager
user.save
user.reload
user.role = :cto # => true
```

----

### 回避策2

`becomes!`メソッドもあるよ
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/persistence.rb

```rb
user.becomes! User::Manager
user.role = :cto # => true
```

----

### 回避策2：説明

* `becomes`は同じメモリ参照するよ
* `becomes!`は新しいインスタンス作るよ

（この辺の用語の正確さには自信がないです…）

```rb

    # Returns an instance of the specified +klass+ with the attributes of the
    # current record. This is mostly useful in relation to single-table
    # inheritance structures where you want a subclass to appear as the
    # superclass. This can be used along with record identification in
    # Action Pack to allow, say, <tt>Client < Company</tt> to do something
    # like render <tt>partial: @client.becomes(Company)</tt> to render that
    # instance using the companies/company partial instead of clients/client.
    #
    # Note: The new instance will share a link to the same attributes as the original class.
    # Therefore the sti column value will still be the same.
    # Any change to the attributes on either instance will affect both instances.
    # If you want to change the sti column as well, use #becomes! instead.
    def becomes(klass)
      became = klass.new
      became.instance_variable_set("@attributes", @attributes)
      became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker)
      became.instance_variable_set("@changed_attributes", attributes_changed_by_setter)
      became.instance_variable_set("@new_record", new_record?)
      became.instance_variable_set("@destroyed", destroyed?)
      became.errors.copy!(errors)
      became
    end

    # Wrapper around #becomes that also changes the instance's sti column value.
    # This is especially useful if you want to persist the changed class in your
    # database.
    #
    # Note: The old instance's sti column value will be changed too, as both objects
    # share the same set of attributes.
    def becomes!(klass)
      became = becomes(klass)
      sti_type = nil
      if !klass.descends_from_active_record?
        sti_type = klass.sti_name
      end
      became.public_send("#{klass.inheritance_column}=", sti_type)
      became
    end
```
----
### 回避策3

バリデーションで頑張った方がいいかも
* inclusionの指定は文字列なのに注意

```ruby:user.rb
class User < ActiveRecord::Base
  enum role: {
    no_role: 0,
    ceo: 110, cto: 120, cfo: 130,
    engineer: 210, hr: 220, account: 230,
  }

  validate :role,
    inclusion: { in: %w(ceo cto cfo) },
    if: -> { manager? }

  validate :role,
    inclusion: { in: %w(engineer hr account) },
    if: -> { worker? }

  def manager?
    type == 'User::Manager'
  end

  def worker?
    type == 'User::Worker'
  end
end
```

```ruby:manager.rb
class User::Manager < User
end
```

```ruby:worker.rb
class User::Manager < User
end
```

----
### 同じ轍を踏んでみた
Validationも同じように再評価が必要っぽい（深追いしてない）

```ruby:user.rb
class User < ActiveRecord::Base
  enum role: {
    no_role: 0,
    ceo: 110, cto: 120, cfo: 130,
    engineer: 210, hr: 220, account: 230,
  }
end
```

```ruby:manager.rb
class User::Manager < User
  validate :role,
    inclusion: { in: %w(ceo cto cfo) }
end
```

```ruby:worker.rb
class User::Manager < User
  validate :role,
    inclusion: { in: %w(engineer hr account) }

end
```

----
# まとめ
----
* enumは便利
* enumはモジュールメソッド
* STIを使うときにはクラスとインスタンスの関係に気をつけよう

----
## 参考
http://apidock.com/rails/ActiveRecord/Enum
http://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html
http://qiita.com/kakipo/items/092cc523849ea324d268
http://itakeshi.hatenablog.com/entry/2014/04/06/131759

```ruby
```