Can I use an ActiveRecord scope as an instance method?
Asked Answered
C

5

17

I have a scope that acts as a filter. For instance:

class User
  scope :in_good_standing, -> { where(:some_val => 'foo', :some_other_val => 'bar' }
end

Because in_good_standing depends on more than one condition, I'd like to have this defined on an instance of User as:

def in_good_standing?
  some_val == 'foo' && some_other_val == 'bar'
end

However, I'd really like to avoid duplicating the logic between both the instance method and the named scope. Is there a way to define #in_good_standing? in a way that simply refers to the scope?

edit

I realize these are very different concepts (one is a class method, one is an instance method), hence my question. As @MrDanA mentioned in a comment, the closest I can get is to check whether or not the record I'm curious about exists within the larger scope, which is probably the answer I'm looking for.

The responses about separating out different scopes from my example are useful, but I'm looking for a general pattern to apply to an application with some very complicated logic being driven by scopes.

Consentient answered 25/4, 2014 at 18:28 Comment(3)
As @meagar stated, you can't, because they are doing very different things. The most you could do is have your instance method call the scope and check to see if it's part of the returned results. However that won't work if the instance has not been saved yet. So in your method you could do: User.in_good_standing.where(:id => self.id).present?Demp
As per your edit, I can add my comment as an answer if that worked for you.Demp
Yes, I'd love your comment as an answer. thanks.Consentient
D
12

Adding my original comment as an answer:

As @meagar stated, you can't, because they are doing very different things. The most you could do is have your instance method call the scope and check to see if it's part of the returned results. However that won't work if the instance has not been saved yet. So in your method you could do:

User.in_good_standing.where(:id => self.id).present? 
Demp answered 30/4, 2014 at 2:32 Comment(1)
This works, but remember that you'll be doing a database query for this, and that can lead to expensive N+1. When I need something like this (imagine scope :active, -> { where("expired_at > ?", Time.current) }, I usually define the scope and repeat it's condition as an instance method (def active?; self.expired_at > Time.current; end); it's not DRY, and if one changes the scope he must remember to also change the instance method (and vice-versa), but your instance method won't issue any extra database calls.Handbarrow
D
12

Scopes are nothing but class methods.You can define it like this

def self.in_good_standing?
  #your logic goes here
end
Doorknob answered 25/4, 2014 at 18:36 Comment(2)
I never thought it this way and it helped a lot.Handbarrow
But you still can't call a class method on an instance like this: user = User.find(3); user.in_good_standing?, so this answer does not help much.Faint
D
12

Adding my original comment as an answer:

As @meagar stated, you can't, because they are doing very different things. The most you could do is have your instance method call the scope and check to see if it's part of the returned results. However that won't work if the instance has not been saved yet. So in your method you could do:

User.in_good_standing.where(:id => self.id).present? 
Demp answered 30/4, 2014 at 2:32 Comment(1)
This works, but remember that you'll be doing a database query for this, and that can lead to expensive N+1. When I need something like this (imagine scope :active, -> { where("expired_at > ?", Time.current) }, I usually define the scope and repeat it's condition as an instance method (def active?; self.expired_at > Time.current; end); it's not DRY, and if one changes the scope he must remember to also change the instance method (and vice-versa), but your instance method won't issue any extra database calls.Handbarrow
Z
4

No, there isn't. One is building a database query, one is working with members of an instantiated object.

Zandrazandt answered 25/4, 2014 at 18:36 Comment(0)
L
1

Yes, you can call the scope from an instance method.

I have been able to call a scope from an instance method using the following pattern:

# Model
class Obj
  # using numeric ids for this example
  # simple scope to return an instance of the record with an id == 1
  scope :get_first_record, -> { find(1) }

  def call_scope
    # I feel using self.class shows the intent of an instance calling its own Class methods, for readability
    self.class.get_first_record
  end
end

# Obj.count => 100
obj = Obj.create # obj.id => 101
obj.call_scope.id # = 1

Having the search logic buried inside a class method could later call for a refactor to allow that logic to be reused elsewhere like in Mrdana's answer.

Therefor, we could do that refactor using my example above, the original scope and using Pavan's point, Mrdana's answer can be rewritten as a scope:

class User
  scope :in_good_standing, -> { where(:some_val => 'foo', :some_other_val => 'bar' }
  scope :users_in_good_standing, -> (users = all) { find(users.map(&:id)).in_good_standing } # same as: User.in_good_standing.where(:id => self.id) when self is the variable

  def in_good_standing?
    self.class.users_in_good_standing(self).present? 
  end
end

This keeps the code DRY, loosely coupled, reusable and extendable; while calling the scope from an instance method.

Leninakan answered 2/4, 2020 at 7:23 Comment(0)
P
0

If you really want to DRY this up, you'd probably need to use a class method instead of a scope (as pavan alludes to). The class method can act the same as a scope and will allow you to use a common bit of code for determining the attributes and values (such as a hash constant?).

But I'd recommend against doing this. This level of abstraction for the sake of DRYness may be a bit over the top. It seems to me that your instance method could be broken up into simpler messages... e.g. good_some_val? and good_some_other_val?. Also because comparing on strings is generally bad / brittle. Anyway, in general, I'd expect you'd want to evolve your object methods and scopes differently. Scopes are the way they are because you're querying a database. So be it for that, but let your objects continue to pass the best messages possible!

Polyhistor answered 25/4, 2014 at 18:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.