説明
よくこういうコードありますよね。
class User < ActiveRecord::Base
# User.admin
scope :admin, -> {
where(role: 'admin')
}
# user.admin? # => true or false
def admin?
self.role == 'admin'
end
end
scope(クラスメソッド)とインスタンスメソッド両方の定義が必要ですが、内部でやっていることは本質的には同じことです。
これをscope :admin
だけ定義しておくと、admin?
も自動的に使えるようになるっていう機能、作ったら需要ありますかねぇ?
https://github.com/amatsuda/arel_ruby
を使ったらできるんじゃないか、という思いつき。複雑なのは無理だろうけど…。
実装してみた
(9/6 追記)
arel_ruby の対応の都合で、Rails 3.2でしか動きません。Rails 4でも動きます!
gem 'arel_ruby'
ActiveRecord::Base.class_eval do
def self.scope(name, scope_options = {})
super
class_eval <<-METHOD
def #{name}?
self.class.#{name}.arel.to_ruby.call([self]).present?
end
METHOD
end
end
たったこれだけで、上のuser.admin?
は余裕で動きます。すごい!
しかし
scope :not_admin, -> {
where('role != "admin"')
}
こうすると、user.not_admin?
は動きません。
文字列で書くとSQLの一部(Arel::Nodes::SqlLiteral)になるので、SQL依存になってしまうのですね。
そこでArel
直接Arelを使えば、複雑な条件もSQL文字列を使わずに記述できます。
scope :not_admin, -> {
where(arel_table[:role].not_eq('admin'))
}
しかしuser.not_admin?
は動かない…。
理由は
-
Cannot visit Arel::Nodes::NotEqual
visit_Arel_Nodes_NotEqualが未実装
- でもこれはEqualityの逆なのですぐ実装できる
- Arel::Nodes::Groupingがつく
- SQLでいう
(...)
。これはRubyではどう表せばいいんだ? - とりあえずこんな感じでいけるっぽい
def visit_Arel_Nodes_Grouping o
v = visit o.expr
ProcWithSource.new("select {|o| o.#{v.to_source}}") { |collection| collection.select {|obj| v.call(obj) } }
end
これで動いたよ。
まだまだ実装されてないvisitorがいっぱいだけど…。
https://github.com/amatsuda/arel_ruby/blob/master/lib/arel/visitors/ruby.rb
ひとまずこっちでがんばって実装してます。だいたいのqueryは動くはず。
https://github.com/tkawa/arel_ruby/blob/master/lib/arel/visitors/ruby.rb
現段階でのまとめ
思いつきにしてはそれなりに動いたし、なかなか良いのでは?
gemつくりました。テストがまだだけど。テストも書きました。
https://github.com/tkawa/activerecord-endoscope