Edited at

単一テーブル継承用の type 列を enum として利用する

More than 3 years have passed since last update.


概要

Rails を使っていてちょくちょくお世話になる 単一テーブル継承 (Single Table Inheritance) (以下 STI) の機能。

この機能を使うと (デフォルトでは) type という名前の列に、その ActiveRecord オブジェクトのクラス名がキャメルケースで設定されます。

そして、Rails 4.1 にて満を持して登場した ActiveRecord::Enum

この STI 用の type 列を enum に流用しようというのが今回の趣旨です。


具体例

例として User モデル (スーパークラス) とそれを継承した Admin, Teacher, Student モデル (サブクラス) を取り上げます。

サブクラスのモデルは Rails の STI を利用して同じ users テーブルにレコードを保存します。

class User < ActiveRecord::Base

end

class Admin < User
end

class Teacher < User
end

class Student < User
end

この場合、enum は User モデルに以下の様に定義します。


user.rb

class User < ActiveRecord::Base

enum type: {
admin: 'Admin',
teacher: 'Teacher',
student: 'Student'
}
end

これで ActiveRecord::Enum が提供する機能を利用できます。

user = Student.create(name: '乃莉')

#=> #<Student id: 1, name: "乃莉" ...>

user[:type]
#=> "Student" # 実際の値を取得する。

user.type
#=> "student" # enum の 値を取得する。

user.teacher?
#=> false

user.student?
#=> true

これで type 列を enum に流用できました!


I18n 対応

enum_help という Gem を利用すると enum の I18n 対応 (日本語対応) が可能です。


ja.yml

ja:

enums:
user:
type:
admin: 管理者
teacher: 教員
student: 学生

User.types_i18n

#=> {"admin"=>"管理者", "teacher"=>"教員", "student"=>"学生"}

user = Student.create(name: 'なずな')
#=> #<Student id: 2, name: "なずな," ...>

user.type_i18n
#=> ...???

しかし、このままでは問題があります。STI を利用しているせいか、サブクラス (例えば Student) 経由で Student.types_i18nStudent#type_i18n を利用しても I18n 対応 (日本語対応) が適用されないのです。

Student.types_i18n

#=> {"admin"=>"admin", "teacher"=>"teacher", "student"=>"student"} # I18n 対応が適用されていない!

user = User.find(2)
#=> #<Student id: 2, name: "なずな," ...>

user.type_i18n
#=> "student" # I18n 対応が適用されていない!


解決策 その1

これは各サブクラスでもスーパークラスと同様に enum の定義をし、

さらに ja.yml も各サブクラスに対応する記述を行えば解決できます。


ja.yml

ja:

enums:
user:
type: &user_type
admin: 管理者
teacher: 教員
student: 学生
student:
type:
<<: *user_type


student.rb

class Student

enum type: User.types
end

Student.types_i18n

#=> {"admin"=>"管理者", "teacher"=>"教員", "student"=>"学生"}

user = User.find(2)
#=> #<Student id: 2, name: "なずな," ...>

user.type_i18n
#=> "学生" # 日本語 キタ━━━━(゚∀゚)━━━━!!

しかし、この方法では言語ファイルの記述やコードの重複が増えてしまうので、別の方法を考えてみました。


解決策 その2

サブクラスの SubClass.types_i18nSubClass#type_i18n をオーバーライドします。

なおメソッドのオーバーライドは、サブクラスがスーパークラスを継承した際に自動的に行われるように Class#inherited を利用して実現します。


user.rb

class User < ActiveRecord::Base

enum type: {
admin: 'Admin',
teacher: 'Teacher',
student: 'Student'
}

# type 列を enum に流用した場合、enum_helper による i18n 対応が効かない模様。
# そのため User.types_i18n, User#type_i18n メソッドを override する。
def self.inherited(klass)
klass.class_eval do
def self.types_i18n
User.types_i18n
end

def type_i18n
User.types_i18n[type]
end
end

super
end
end


これでサブクラスが増えても安心ですね!