ActiveRecordにこういう機能があったらどうでしょう?をgemにしてみた

  • 23
    Like
  • 0
    Comment
More than 1 year has passed since last update.

https://github.com/tkawa/activerecord-endoscope

説明

よくこういうコードありますよね。

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'
config/initializers/scope_ext.rb
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ではどう表せばいいんだ?
    • とりあえずこんな感じでいけるっぽい
lib/arel/visitors/ruby.rb
      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