先日微妙にハマってしまったRailsの小ネタです。
ActiveRecordは次のようにjoins
メソッドを使って別のモデル(テーブル)をJOINすることができます。
Task.joins(:project)
また、includes
メソッドなどはシンボルを渡しても文字列を渡しても同じ挙動になります。
# includesメソッドはシンボルでも文字列でも挙動は同じ
Task.includes(:project)
Task.includes('project')
これと同じようにjoins
も同じようにシンボルでも文字列でも渡せるよね?と思ったんですが、文字列だとエラーが出ました。
Task.joins('project')
# Task Load (0.2ms) SELECT "tasks".* FROM "tasks" project
# .../sqlite3/database.rb:152:in `initialize': SQLite3::SQLException: no such table: tasks (ActiveRecord::StatementInvalid)
# .../sqlite3/database.rb:152:in `initialize': no such table: tasks (SQLite3::SQLException)
なぜエラーが出たのか?
joins
メソッドにシンボルを渡すとそれは関連名として扱われます。
ですが、文字列を渡すと独自のJOIN用SQLとして扱われます
# 引用: https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-joins
User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
# SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
そのため、文字列の"project"を渡すと予期せず無効なSQLが発行され、エラーが起きるのです。
# joinsにシンボルを渡した場合(関連名として扱われる)
puts Task.joins(:project).to_sql
# SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id"
# joinsに文字列を渡した場合(tasksテーブルにprojectという別名が予期せず付いてしまう)
puts Task.joins('project').to_sql
# SELECT "tasks".* FROM "tasks" project
うっかりミスと言えばうっかりミスなのですが、「シンボルを渡しても文字列を渡しても、内部的に型が統一されるのでどっちでもOK」というケースはRailsを使っているとよくあることなので、てっきりjoins
メソッドもそんなふうに動いてくれるもんだと勘違いしていました。
ふつうにjoins
メソッドを使うときはシンボルで関連名を指定すると思いますが、プログラム上で動的にJOINするテーブルを指定する場合に、予期せず文字列で関連名を渡したりすることがあるかもしれません。みなさんも気を付けてください。