2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

rails7.1から単数形で定義されている関連付けに対して`where`で複数形で指定して絞り込みする方法ができなくなるので注意する

Last updated at Posted at 2023-06-15

具体例

以下の様に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.0rails7.1も同様です。

rails 7.0までの回避方法

rails 7.0では、keyroleではなく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 })とする

発行されるSQLrails7.0rails7.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までの振る舞いを維持できます。

config/application.rb
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について

元々wherehashが入力された場合に関連付けの指定なのか判断する際に以下の2つをチェックしていました。

  1. keyが関連付けの定義に存在するか
  2. keysingularizeした値が関連付けの定義に存在するか

2のsingularize処理のパフォーマンスが悪い事がわかったため、2. keysingularizeした値が関連付けの定義に存在するか自体を実行しない様にする事で、singularizeの呼び出しを無くしパフォーマンスを向上させた様です。尚、この対応で2倍程のパフォーマンスの向上が計測できたとのことです。

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?