要点
- joinsで内部結合してselectしたデータを、enumで比較しようとしてハマった
- 内部結合してselectした際、基準となるテーブル以外のテーブルは、enum定義されててもデータベースに登録されている素の値が出る模様
確認した環境
- Ruby 2.5.5
- Rails 5.2.3
- MariaDB 10.4.8
例
class Company < ApplicationRecord
has_many :offices
end
class Office < ApplicationRecord
belongs_to :company
has_many :employees
end
class Employee < ApplicationRecord
belongs_to :office
enum type: {
regular: 0, # 正社員
contract: 1, # 契約社員
temporary: 2, #派遣
}
※実際に上のモデルで動作確認してないので、挙動異なったらすみません。
起きた問題
「A社の社員に関して、社員の契約形態ごとの情報を扱いたいやで。あ、オフィスの付属情報もほしいから、selectして頼むわ」
ほんほん、なるほどな〜〜
office_employees = Company.find_by(name: "A")
.offices.joins(:employees)
.select("offices.*, employees.name, employees.type")
「eachしてtypeごとにif文書いてくれやで」
おう、こうやろ?
office_employees.each do |employee|
if employee.regular?
...
elsif ...
end
end
ん、regular?はあかんのか、じゃあこうか。
office_employees.each do |employee|
if employee.type == 'regular'
...
elsif ...
end
end
んん……全部elseに入ってしまう……。
ちょっとpryで止めて見てみるか……。
pry(main)> employees.first.type
=> 0
enumの文字列じゃなくてDBの数字そのまま返ってくるやんけ!
動作確認
pry(main)> Employee.first.type
=> 'regular'
pry(main)> employees.first.type
=> 0
直接呼び出した場合はenum定義した文字列で返ってくるが、関連テーブルのjoins select経由の場合はデータベースに入っている数字がそのまま返ってくる。
ちなみに基準となるテーブル(この例の場合Office)のenumは、selectしていてもそのままenumで扱える模様。
Railsガイド、RailsAPIを読む
ふわっとした認識で使ってたこれらのメソッドに関してリファレンスをちゃんと読んでみる。
enum
enumマクロは整数のカラムを、設定可能な値の集合にマッピングする。
対応するスコープ、及び状態の遷移や現在の状態の問い合わせ用のメソッドが自動で追加される。
enum status: [:active, :archived]
のように配列形式で指定した場合は、配列のindexが数値として設定される。
そのため、配列の順序を維持することが必須で、新規追加する場合は配列の最後に追加すること。
ハッシュで明示的にマッピングすることが推奨される。(今回の例の場合はそうしてる)
select
selectメソッドを使うと、関連付けられたオブジェクトのデータ取り出しに使われるSQLのSELECT句を上書きする。Railsはデフォルトではすべてのカラムを取り出す。
配列を返すように見えるが、実際はリレーションオブジェクトを返している。
メソッドの引数は、フィールドの配列にすることもできる。文字列を使用した場合は、変更されずにSELECTフィールドとして使用される。
pry(main)> employees.class
=> Office::ActiveRecord_AssociationRelation
pry(main)> employees.first.class
=> Office(id: integer, ......)
恐らくこのリレーションオブジェクトが結合元のモデルを基準にして結合したものなので、その結合元のモデルに定義のないenumはDBの値そのまま出す形なのだろう。
enumの定義のないモデルにenumで呼びかけている、って考えたらまぁ当たり前なのかもしれないが、何も考えないで使っているとハマる……(普段はenum意識せずに文字列として使ってる場合もままあるので)。
対応
office_employees.each do |employee|
if employee.type == Employee.types["regular"]
...
elsif ...
end
end
Employee.types["regular"]の返り値はenum定義した数値なので、上記例の場合はこれで対応可能。
便利なものだからとAPIとかあんま読まずに雰囲気で使ってるとだめですねー。
余談
後で知ったが上記例の場合、selectでなくpluckを使うほうが、リレーションオブジェクトを作らないため速くなるし安全な模様。
pluckにsql書く発想がなかった。