コーディング中に、特定の条件の時だけwhere句を有効にしたい!と思うことがありました。
最初は単純にif文で分岐していたのですが、あまりにもダサいと思ったので奮闘してみました。
サンプルコード
if kind == 'hoge'
User.where(kind: kind)
else
User.all
end
ActiveRecordを拡張
こちらの記事を参考に、ActiveRecordを拡張してみました。
記事内の拡張コードはRails5では動かなかったので、Railsのコードを読みつつ以下のように拡張しました。
拡張コード
ActiveRecord::QueryMethods::WhereChain.include(Module.new do
def if(condition, opts, *rest)
return @scope unless condition
@scope.where_clause += @scope.send(:where_clause_factory).build(opts, rest)
@scope
end
end)
サンプルコード
kind = 'hoge'
User.where.if(kind == 'hoge', kind: kind).to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`kind` = hoge"
kind = 'fuga'
User.where.if(kind == 'hoge', kind: kind).to_sql
=> "SELECT `users`.* FROM `users`"
いい感じですね
レビューで却下される
ドヤ顔でプルリクを送るもレビューで以下の指摘を受けました。
- Railsのバージョンが上がった時このコードが動く保証がない
- 現にRails4の拡張コードがRails5で動かなかった
- Rails標準機能でないため、初めてこのコードを見た人が戸惑うのではないか
- 驚き最小の原則は重要
ごもっとも。。。としか言えなかったので拡張コードの採用は見送りました。
yield_selfを使う
もうif文で書くかと諦めかけていたところ、yield_selfを使ってはどうかと助言をいただきました。
yield_selfを使ったコードがこちら。
サンプルコード
kind = 'hoge'
User.yield_self{|scope| kind == 'hoge' ? scope.where(kind: kind) : scope.all }.to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`kind` = hoge"
kind = 'fuga'
User.yield_self{|scope| kind == 'hoge' ? scope.where(kind: kind) : scope.all }.to_sql
=> "SELECT `users`.* FROM `users`"
以下のようにQueryMethodを繋げることもできます。
サンプルコード
kind = 'hoge'
User.yield_self{|scope| kind == 'hoge' ? scope.where(kind: kind) : scope }.where(age: 0..10).to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`kind` = hoge AND (`users`.`age` BETWEEN 0 AND 10)"
kind = 'fuga'
User.yield_self{|scope| kind == 'hoge' ? scope.where(kind: kind) : scope }.where(age: 0..10).to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`age` BETWEEN 0 AND 10)"
yield_selfを使ったコードは無事LGTMしていただきました
まとめ
- yield_self便利
- チーム開発で拡張は控えた方が無難かも