Rails

Using lambda in ActiveRecord scopes that involve other classes


On one occasion, I had a problem with a couple of ActiveRecord scopes. The problem showed up when creating the database (rake db:create) and running migrations (rake db:migrate).

The problem showed up in Travis CI after creating a new page with some reports. On this report page I used two related models. Example:

#
# models/story.rb
#

class Story < ActiveRecord::Base
    belongs_to: :retrospective

    scope :externals, lambda { includes(:retrospective).
        where('retrosopective.id in (?)', Retrospective.externals.map(&:id)) }
end

#
# models/retrospective.rb
#

class Retrospective < ActiveRecord::Base
   has_many: stories

   scope :externals, where(owner_id: tableX.external_users)

end

We can observe that the scope in table1 and this scope searches for some external data and uses the relation with the other model internally (table2). Everything works fine in our local environmente because the database and the tables exist, but, when I wanted to deploy the new functionality to our production servers, I was greeted by an error:

$ cp config/database.yml.travis config/database.yml
$ bundle exec rake db:create:all db:schema:load | tail
rake aborted!
PG::Error: ERROR:  relation "retrospectives" does not exist
LINE 4: WHERE a.attrelid = '"retrospectives"'::regclass ^
   SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
       FROM pg_attribute a LEFT JOIN pg_attrdef d
          ON a.attrelid = d.adrelid AND a.attnum = d.adnum
       WHERE a.attrelid = '"retrospectives"'::regclass
       AND a.attnum > 0 AND NOT a.attisdropped
       ORDER BY a.attnum

The reason

Before running any rake task, scopes are being evaluated, so, any dependency with another model or table that is not created before will result in an error.

The solution

In all your scopes, where you have a dependency with another model, you must evaluate them with lambda. In my case, it works after I added lambda { after the name of the scope and its respective separation comma:

#
# models/story.rb
#

class Story < ActiveRecord::Base
    belongs_to: :retrospective

    scope :externals, lambda { includes(:retrospective).
        where('retrosopective.id in (?)', Retrospective.externals.map(&:id)) }
end

What lambda does, is that the code in the scope is not being evaluated until the moment the scope is actually used.

Another weird thing that I noticed is that, when using sqlite, the error was very different. All it showed was rake aborted!.

The even better solution

Actually, the best solution to this probelm is to use class methods instead of scopes:

#
# models/story.rb
#

class Story < ActiveRecord::Base
  def self.externals
    includes(:retrospective).where('retrospective.id in (?)', Retrospective.externals.map(&:id))
  end 
end

Thank you for reading.

Best Practices
De Código, Café y Cervezas 07 – ¿Somos profesionales?
Events
RailsConf 2017 Adventure
Beginner
Administrate review