Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails - Including associations with dynamic conditions

Given a school model and a student model with the school having a has_many relation to student:

has_many :students, :conditions => proc  { "year_id=#{send(:active_year_id)}" }

where active_year_id is a method defined in the school model, I'm encountering an error that "active_year_id is undefined" when calling:

School.where(:active => true).includes(:students)

The condition works fine when I do, say,

School.where(:id => 10).students

Only when I try to use includes do I get that error. Is that the right behavior. If not, what am I doing wrong and how do I fix ?

Using Rails 3.0.9, REE 1.8.7.

like image 714
Vijay Dev Avatar asked Jul 13 '11 19:07

Vijay Dev


2 Answers

This is a bit old, but I see the question a lot and have not seen any satisfactory solutions. Adding a condition like this is essentially equivalent to creating an association with two public/private keys.

@Fabio is right in saying "the context in where the proc is executed is different depending of how it's called." However I think you can overcome the "active_year_id is undefined" problem.

In the example:

class School < ActiveRecord::Base
  has_many :students, :conditions => proc  { "year_id=#{send(:active_year_id)}" }
  ...

the issue is that in some situations, the proc is executed in the context of a particular School object and sometimes as an ActiveRecord::Associations::JoinDependency::JoinAssociation. I solved this using a slightly more complicated proc, like this:

class School < ActiveRecord::Base
  has_many :students, :conditions => proc  { 
      self.respond_to?(:active_year_id) ?
        {year_id: self.active_year_id} : 
        'students.year_id = schools.active_year_id'
    }

So, when the condition is calculated for an actual school object, self responds to the active_year_id attribute accessor and you can provide a hash as the condition (which works more nicely than just an interpolated string for creating associated objects, etc.)

When the context does not present self as an actual school object, (which, as you noted, occurs when called using an include clause, for example,) the context is a JoinAssociation and the string form of the condition works just fine using field names rather than values.

We found this solution let us use the dynamic association successfully.

like image 134
Tom Wilson Avatar answered Nov 15 '22 20:11

Tom Wilson


I think this is not possible to achieve because the context in where the proc is executed is different depending of how it's called. I've made a basic app with your models and this is what happens when you call the various methods (ap is this):

class School < ActiveRecord::Base
  has_many :students, :conditions => proc { ap self; "year_id=#{send(:active_year_id)}" }  
end

When you call the students relation from a school instance the context of the proc is the given School instance so it does respond to the active_year_id method

[31] pry(main)> School.first.students
  School Load (0.2ms)  SELECT "schools".* FROM "schools" LIMIT 1
#<School:0x007fcc492a7e58> {
                :id => 1,
              :name => "My school",
    :active_year_id => 1,
           :year_id => 1,
        :created_at => Tue, 08 May 2012 20:15:19 UTC +00:00,
        :updated_at => Tue, 08 May 2012 20:15:19 UTC +00:00
}
  Student Load (0.2ms)  SELECT "students".* FROM "students" WHERE "students"."school_id" = 1 AND (year_id=1)
+----+----------------+-----------+---------+-------------------------+-------------------------+
| id | name           | school_id | year_id | created_at              | updated_at              |
+----+----------------+-----------+---------+-------------------------+-------------------------+
| 1  | My student     | 1         | 1       | 2012-05-08 20:16:21 UTC | 2012-05-08 20:16:21 UTC |
| 2  | Second student | 1         | 1       | 2012-05-08 20:18:35 UTC | 2012-05-08 20:18:35 UTC |
+----+----------------+-----------+---------+-------------------------+-------------------------+
2 rows in set

But when you call the includes relation the context is different and what the proc receive as self is Student class, so it doesn't respond to that method and this will trigger the error

[32] pry(main)> School.includes(:students).all
  School Load (0.3ms)  SELECT "schools".* FROM "schools" 
class Student < ActiveRecord::Base {
            :id => :integer,
          :name => :string,
     :school_id => :integer,
       :year_id => :integer,
    :created_at => :datetime,
    :updated_at => :datetime
}
NoMethodError: undefined method `active_year_id' for #<Class:0x007fcc4a6a3420>
from /Users/fabio/.rvm/gems/ruby-1.9.3-p194/gems/activerecord-3.2.3/lib/active_record/dynamic_matchers.rb:50:in `method_missing'

I think that a has_many relation can't be used with that kind of proc which relies on an instance method of a School instance. I think that the only way to use procs as described here is to compute some condition at runtime which doesn't involve instance methods (time conditions, where with data from unrelated models and so on).

Moreover the School.includes(:students).all in my example cannot work because it should call the active_year_id method on every instance of School (which should be retrieved from db before the includes could be evaluated) and thus vanishing the effect of the includes intended behavior.

All of this is valid if the active_year_id is a computed method defined in School class based on instance data. Instead if the active_year_id is not a method but a field (a db column) of the School class you can play with joins and scopes to achieve a result similar to what you want to achieve but it should be coded by hand.

like image 30
Fabio Avatar answered Nov 15 '22 21:11

Fabio