読者です 読者をやめる 読者になる 読者になる

プログラミング 美徳の不幸

Ruby, Rails, JavaScriptなどのプログラミングまとめ、解説、備忘録。

railsの

今日元食べログの大エンジニアとお話していて、scopeっていけてないねっていう話になった。

scopeとは以下のようなもので

class User < AR::Base
  has_many :blogs
  has_many :fav_stars, through: :blogs

  scope :recent, -> { order('created_at DESC') }
  scope :has_blog, -> { joins(:blogs).group('users.id') }
end

class Blog < AR::Base
  has_many :fav_stars

  scope :favorites, -> { joins(:fav_stars).group('blogs.id').having("count(fav_stars.id) > 10") }

とかしておくと最近のユーザで人気のブログ書いてる人が

User.recent.joins(blogs: :fav_stars).merge(Blog.favorites)

とかでとれるようになる(はず)。



でありがちなのは上のメソッドチェーンを

scope :recent_favorite_users -> { recent.joins(blogs: :fav_stars).merge(Blog.favorites) }

とかでとれるようにすることだけど、これが良くないらしくてそもそもSQLのパーツであるscopeにrecent_favorite_usersのようなビジネスロジックを組み込むのは良くない、ということらしい。

あまり深く考えたことなかったけどこのscopeって確かにいろいろ欠陥があって、ActiveRecord::Relation#mergeとかいろいろあるくせになんだかんだいって複数のmodelのscopeを組み立てていくみたいなことすると途端に破綻する。

例えば
name_is_hoge -> { where(name: 'hoge') }っていうscopeならいいんだけど、having_name -> { where('name IS NOT NULL') }っていうscopeだとダメ、みたいな。

これはどういうことかというとUser.joins(blogs: :fav_stars).having_nameとかするとjoinしてるblogとかfav_starにnameっていうカラムがあるとambiguous column nameっていうエラーが出ちゃう。

where(name: 'hoge')は厳密にはwhere("name = 'hoge'")とは等価ではなくwhere("users.name = 'hoge'")と等価ということですね。



で、まぁここらへんの運用をきっちりしましょうというのもあるんだけど、そもそもいろんなmodelに渡るビジネスロジックってけっこう重要じゃないですか? というわけでmergeだの複雑なscopeチェーンがある場合は全部クラスメソッドにすべきじゃないですか? というご意見でした。

メモまでに。