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 environments 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 with lambda
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 problem 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.