端的に言えば、Association
からクラスメソッドを呼ぶと、Association
に属するレコード群に対してメソッドが呼ばれて少しハマったという話。
やりたかったこと
ユーザーが商品を購入するサービスで、商品毎の購入者番号(Buyer.number
)を購入順で振りたい。
実装
Model
サービス利用者であるUser
は、購入者情報であるBuyer
を複数持ち、Buyer
はUser
とProduct
に結びつく
app/model/user.rb
class User < ActiveRecord::Base
has_many :buyers
app/model/buyer.rb
class Buyer < ActiveRecord::Base
belongs_to :user
belongs_to :product
# Productに対する購入者番号を自動で割り振りつつRecordを生成
def self.create_by_product(product)
create(product_id: product.id, number: current_provided_number(product))
end
# 現在割り振られる購入者番号
# Productの購入者がいないなら1を、いるなら現在の最大購入者番号+1を返す
def self.current_provided_number(product)
if exists?(product_id: product.id)
current_last_buyer = where(product_id: product.id).order('number ASC').last
current_last_buyer.number + 1
else
1
end
end
Controller
購入時にUser
にひも付ける形でBuyer
を生成
app/controller/products_controller.rb
product.with_lock { user.buyers.create_by_product(product) }
実行結果
期待してた挙動
app/model/buyer.rbif exists?(product_id: product.id) current_last_buyer = where(product_id: product.id).order('number ASC').last
のif節内で
WHERE `buyers`.`product_id` = 12 LIMIT 1
のようなクエリが飛ぶと思っていた。
実際の挙動
WHERE `project_sponsors`.`user_id` = 3 AND `project_sponsors`.`project_id` = 12 LIMIT 1
これが飛んだ。
原因と解決策
user.buyers.create_by_product(product)
といった呼び方をしたため、意図せずuser_id
でも絞り込みがかかってしまった。
解決策としては、基本的に呼び出す側で気をつけましょうというのが一番正しいと思う。
ただ、絶対にAssociation
ではなくてActiveRecord
そのものに対して呼びたいメソッドである場合、unscoped
使うとかもありなのかも。
app/model/buyer.rb
class Buyer < ActiveRecord::Base
...
# 現在割り振られる購入者番号
# Productの購入者がいないなら1
# いるなら最大購入者番号+1を返す
def self.current_provided_number(product)
unscoped do # どこから呼ばれようとBuyerそのものに対してクエリを投げる
if exists?(product_id: product.id)
current_last_buyer = where(product_id: product.id).order('number ASC').last
current_last_buyer.number + 1
else
1
end
end
end
こんな感じで。
ひょっとしたらアンチパターンかもしれないけど。