Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 3 merging scopes with joins

Setup

For this question, I'll use the following three classes:

class SolarSystem < ActiveRecord::Base
  has_many :planets

  scope :has_earthlike_planet, joins(:planets).merge(Planet.like_earth)
end

class Planet < ActiveRecord::Base
  belongs_to :solar_system
  belongs_to :planet_type

  scope :like_earth, joins(:planet_type).where(:planet_types => {:life => true, :gravity => 9.8})
end

class PlanetType < ActiveRecord::Base
  has_many :planets

  attr_accessible :gravity, :life
end

Problem

The scope has_earthlike_planet does not work. It gives me the following error:

ActiveRecord::ConfigurationError: Association named 'planet_type' was not found; perhaps you misspelled it?

Question

I have found out that this is because it is equivalent to the following:

joins(:planets, :planet_type)...

and SolarSystem does not have a planet_type association. I'd like to use the like_earth scope on Planet, the has_earthlike_planet on SolarSystem, and would like to avoid duplicating code and conditions. Is there a way to merge these scopes like I'm attempting to do but am missing a piece? If not, what other techniques can I use to accomplish these goals?

like image 497
Aaron Avatar asked Oct 23 '12 18:10

Aaron


3 Answers

Apparently, at this time you can only merge simple constructs that don't involve joins. Here is a possible workaround if you modify your models to look like this:

class SolarSystem < ActiveRecord::Base
  has_many :planets
  has_many :planet_types, :through => :planets

  scope :has_earthlike_planet, joins(:planet_types).merge(PlanetType.like_earth)
end

class Planet < ActiveRecord::Base
  belongs_to :solar_system
  belongs_to :planet_type

  scope :like_earth, joins(:planet_type).merge(PlanetType.like_earth)
end

class PlanetType < ActiveRecord::Base
   has_many :planets

   attr_accessible :gravity, :life

   scope :like_earth, where(:life => true, :gravity => 9.8)
end

** UPDATE **

For the record, a bug was filed about this behavior - hopefully will be fixed soon...

like image 180
PinnyM Avatar answered Oct 18 '22 00:10

PinnyM


You are reusing the conditions from the scope Planet.like_earth, which joins planet_type. When these conditions are merged, the planet_type association is being called on SolarSystem, which doesn't exist.

A SolarSystem has many planet_types through planets, but this is still not the right association name, since it is pluralized. You can add the following to the SolarSystem class to setup the planet_type association, which is just an alias for planet_types. You can't use the Ruby alias however since AREL reflects on the association macros, and doesn't query on whether the model responds to a method by that name:

class SolarSystem < ActiveRecord::Base
  has_many :planets
  has_many :planet_types, :through => :planets
  has_many :planet_type, :through => :planets, :class_name => 'PlanetType'

  scope :has_earthlike_planet, joins(:planets).merge(Planet.like_earth)
end

SolarSystem.has_earthlike_planet.to_sql # => SELECT "solar_systems".* FROM "solar_systems" INNER JOIN "planets" ON "planets"."solar_system_id" = "solar_systems"."id" INNER JOIN "planets" "planet_types_solar_systems_join" ON "solar_systems"."id" = "planet_types_solar_systems_join"."solar_system_id" INNER JOIN "planet_types" ON "planet_types"."id" = "planet_types_solar_systems_join"."planet_type_id" WHERE "planet_types"."life" = 't' AND "planet_types"."gravity" = 9.8
like image 43
Ben Simpson Avatar answered Oct 18 '22 02:10

Ben Simpson


An easy solution that I found is that you can change your joins in your Planet class to

joins(Planet.joins(:planet_type).join_sql)

This will create an SQL string for the joins which will always include the correct table names and therefore should always be working no matter if you call the scope directly or use it in a merge. It's not that nice looking and may be a bit of a hack, but it's only a little more code and there's no need to change your associations.

like image 27
Alexander Weber Avatar answered Oct 18 '22 00:10

Alexander Weber