2

I have a active record, where I have a usecase: I want to use map in the ActiveRecord scopes. Following is my code:

class SupportedGeoIdAndLocation < ActiveRecord::Base

   scope :supported_geo_ids,            lambda { all.map(&:geo_id).uniq }
   # also tried: scope :supported_geo_ids,            lambda { select('distinct geo_id').map(&:geo_id).uniq }
   scope :locations_for_geo_id, lambda { |geo_id| where(:geo_id => geo_id).map(&:location) }
end

but when I do SupportedGeoIdAndLocation.supported_geo_ids I get empty array, while SupportedGeoIdAndLocation.all.map(&:geo_id).uniq gives me desired result.

I am looking for a way so that this can be done using lambda in one go, not via chaining of different methods and completely avoiding class functions.

Looks like I am using scope in wrong way, Please suggest me what can be correct way.

version: padrino:0.10.5, ruby: ruby-1.9.3-p429, ActiveRecord: 3.1.6

Saurabh
  • 71,488
  • 40
  • 181
  • 244

3 Answers3

5

scope should return an ActiveRecord::Relation, so scope could be chained with another scope or class methods etc.

If you want to get an array, use class method instead.

def self.supported_geo_ids
  all.map(&:geo_id).uniq
end
xdazz
  • 158,678
  • 38
  • 247
  • 274
3

After playing around with some similar code in one of my models I finally found this question (as I got the same error message (in Rails 3.2 that was)). The accepted answer states that ActiveRecord implicitly assumes scopes to be "chainable" (in ARel sense) which the array you return obviously is not.

So while you can do whatever you want in a class method you define yourself (as in the answer of @xdazz) you will have to adhere to ActiveRecord conventions if you insist on using scope (which at first glance just defines a class method with whatever lambda you provided it). There seems to be more to this than I could spot.

Update

After some more fiddling I think I got as close as it gets. You can write a scope that returns just supported geo ids, i.e. as follows:

class SupportedGeoIdAndLocation < ActiveRecord::Base
  scope :supported_geo_ids,  lambda { select("distinct(geo_id)") }
end

However that will return an ARel relation that can be turned into an array of SupportedGeoIdAndLocation objects with just the geo_id attribute set (and, sorry to disappoint you, it is a class method, even if it ends up being defined through the scope class method). So if you need an array of attributes you can turn it into one using the method you proposed in another class method:

class SupportedGeoIdAndLocation < ActiveRecord::Base
  def self.geo_ids_array
    supported_geo_ids.map(&:geo_id)
  end
end

Note that there is no need to call uniq now as this has been taken care of by the distinct in the scope. Up from rails 4.0.2 ActiveRecord does support the distinct method by the way, so you won't have to resort to using a string if you can update your ActiveRecord gem as Padrino should not depend on it.

Community
  • 1
  • 1
Patru
  • 4,481
  • 2
  • 32
  • 42
3

As @patru pointed out, scopes are only meant to return relations. They are not meant to return values. Therefore, your question "how do I make a scope return a particular value?" is unanswerable.

My impression is that you think that class methods are poor design, or not "The Rails Way", but in this case, I agree with @xdazz that they are the best solution.

However, I would implement the method slightly differently:

def self.supported_geo_ids
  self.all(:select => "DISTINCT(geo_id)").map(&:geo_id)
end

That way, de-duplicating is performed in the database engine, and only the required values are returned. This makes the query potentially much faster.

In newer versions of Rails, you would write it like this:

def self.supported_geo_ids
  self.uniq.pluck(:geo_id)
end

If for some reason you still don't want to use a class method, please explain why and I'll try to think of a workaround.

Nick Urban
  • 3,568
  • 2
  • 22
  • 36