具体例
以下の様にUser
が中間テーブルを介して1つのRole
を持つ定義をしている場合
class Role < ApplicationRecord
end
class User < ApplicationRecord
has_one :user_role
has_one :role, through: :user_role
end
class UserRole < ApplicationRecord
belongs_to :user
belongs_to :role
end
上記のモデル定義に対して特定のRole
を持っているUser
を絞り込む際に関連付けの定義名role
を使ってデータ取得を行おうとすると以下の様にエラーが発生します。
role = Role.first
User.joins(:role).where(role: role).to_sql
"SELECT `users`.* FROM `users` INNER JOIN `user_roles` ON `user_roles`.`user_id` = `users`.`id` INNER JOIN `roles` ON `roles`.`id` = `user_roles`.`user_id` WHERE `role`.`id` = 64"
#=>#<ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'role.id' in 'where clause'> rescued during inspection
where
で指定されたrole
を元にクエリを生成しrole
テーブルが存在しないためエラーになっています。
この振る舞い自体はrails7.0
もrails7.1
も同様です。
rails 7.0までの回避方法
rails 7.0
では、key
にrole
ではなくroles
を指定する事でWHERE roles.id = ?
が発行しデータ取得を行うことができました。
role = Role.first
User.joins(:role).where(roles: role).to_sql
"SELECT `users`.* FROM `users` INNER JOIN `user_roles` ON `user_roles`.`user_id` = `users`.`id` INNER JOIN `roles` ON `roles`.`id` = `user_roles`.`user_id` WHERE `roles`.`id` = 64"
ただし、この方法は意図した機能として提供されているわけではなくたまたま動いていた事象に近い様です。
本来roles
は関連付けで定義していないが、roles
の単数形であるrole
が関連付けに定義されているためwhere
メソッド内の処理でroles
も関連付けの指定だと判断されていました。
rails 7.1
本来
roles
は関連付けで定義していないが、roles
の単数形であるrole
が関連付けに定義されているためwhere
メソッド内の処理でroles
も関連付けの指定だと判断されていました。
rails 7.1
の場合は上記のロジックが削除されたので、rails7.0
で動作していた回避策は動作しなくなりました。roles
を指定すると関連付けではなくusers
テーブルのカラムとして検索するクエリが発行されるため、WHERE users.roles = ?
となりカラムが存在しないエラーが発生します。
role = Role.first
User.joins(:role).where(roles: role).to_sql
"SELECT `users`.* FROM `users` INNER JOIN `user_roles` ON `user_roles`.`user_id` = `users`.`id` INNER JOIN `users` ON `users`.`id` = `user_roles`.`user_id` WHERE `users`.`roles` = 67"
#<ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'users.roles' in 'where clause'> rescued during inspection
対応
1. where(role: { id: role.id })
とする
発行されるSQL
はrails7.0
とrails7.1
間で同様です。
意図したデータを取得することができます。
# rails 7.0
role = Role.first
User.joins(:role).where(role: { id: role.id }).to_sql
#=> "SELECT `users`.* FROM `users` INNER JOIN `user_roles` ON `user_roles`.`user_id` = `users`.`id` INNER JOIN `roles` `role` ON `role`.`id` = `user_roles`.`role_id` WHERE `role`.`id` = 5"
# rails 7.1
role = Role.first
User.joins(:role).where(role: { id: role.id }).to_sql
#=> "SELECT `users`.* FROM `users` INNER JOIN `user_roles` ON `user_roles`.`user_id` = `users`.`id` INNER JOIN `roles` `role` ON `role`.`id` = `user_roles`.`role_id` WHERE `role`.`id` = 5"
2. config
の設定値で互換性を保つ
rails7.1
では互換性を保つための設定が用意されているので使用する事でrails7.0
までの振る舞いを維持できます。
Rails.application.config.active_record.allow_deprecated_singular_associations_name = true
Rails.application.config.active_record.allow_deprecated_singular_associations_name = true
にすると、エラーではなく以下のDEPRECATION WARNING
の出力を行いrails7.0
以前の振る舞いを維持します。
role = Role.first
User.joins(:role).where(roles: role)
#=> <ActiveSupport::DeprecationException: DEPRECATION WARNING: Referring to a singular association (e.g. `user`) by its plural name (e.g. `users`) is deprecated.
#
# To convert this deprecation warning to an error and enable more performant behavior, set config.active_record.allow_deprecated_singular_associations_name = false.
変更されたPRについて
元々where
でhash
が入力された場合に関連付けの指定なのか判断する際に以下の2つをチェックしていました。
-
key
が関連付けの定義に存在するか -
key
をsingularize
した値が関連付けの定義に存在するか
2のsingularize
処理のパフォーマンスが悪い事がわかったため、2. key
をsingularize
した値が関連付けの定義に存在するか自体を実行しない様にする事で、singularize
の呼び出しを無くしパフォーマンスを向上させた様です。尚、この対応で2倍程のパフォーマンスの向上が計測できたとのことです。