ActiveRecord, Rails 4: has_many :through with scoped conditions failure

210 views Asked by At

I'm working on a system where a User can be associated with another entity, a Wedding, in multiple ways through an Attendance model using user_id, wedding_id and role information. With help from this question, I sort of found my way of replacing :conditions with a Rails 4-style association. My associations are now these:

User:

has_many  :wedding_attendances
has_many  :weddings,
          through: :wedding_attendances
has_many  :marriages,
          -> { WeddingAttendance.find_by_role(:spouse) },
          through: :wedding_attendances,
          class_name: 'Wedding'
has_many  :invitations,
          -> { WeddingAttendance.find_by_role(:invitee) },
          through: :wedding_attendances,
          class_name: 'Wedding'
has_many  :ceremonies,
          -> { WeddingAttendance.find_by_role(:worker) },
          through: :wedding_attendances,
          class_name: 'Wedding'

Wedding:

has_many  :wedding_attendances
has_many  :attendees,
          through: :wedding_attendances,
          class_name: 'User'
has_many  :spouses,
          -> { WeddingAttendance.find_by_role(:spouse) },
          through: :wedding_attendances,
          class_name: 'User'
has_many  :invitees,
          -> { WeddingAttendance.find_by_role(:invitee) },
          class_name: 'User',
          through: :wedding_attendances
has_many  :workers,
          -> { WeddingAttendance.find_by_role(:worker) },
          class_name: 'User',
          through: :wedding_attendances

WeddingAttendance:

belongs_to :wedding
belongs_to :user

class << self
  def roles
    {
      spouse: 0,
      invitee: 1,
      worker: 2
    }
  end

  def find_by_role role
    if role.class == Symbol
      where(role: roles[role])
    else
      where(role: role)
    end
  end
end

Not as straightforward as I wanted, but apparently correct. Or not, because User.first.weddings, for example, raises the following error, even though WeddingAttendance.find_by_role(:spouse) returns a correct result set:

NoMethodError: undefined method `to_sym' for nil:NilClass
        from c:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activerecord-4.1.8/lib/active_record/reflection.rb:100:in `_reflect_on_association'
        from c:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activerecord-4.1.8/lib/active_record/reflection.rb:537:in `source_reflection'
        from c:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activerecord-4.1.8/lib/active_record/reflection.rb:697:in `check_validity!'
        from c:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activerecord-4.1.8/lib/active_record/associations/association.rb:25:in `initialize'
        from c:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activerecord-4.1.8/lib/active_record/associations/has_many_through_association.rb:9:in `initialize'
        from c:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activerecord-4.1.8/lib/active_record/associations.rb:155:in `new'
        from c:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activerecord-4.1.8/lib/active_record/associations.rb:155:in `association'
        from c:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activerecord-4.1.8/lib/active_record/associations/builder/association.rb:110:in `marriages'
        from (irb):51
        from c:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/railties-4.1.8/lib/rails/commands/console.rb:90:in `start'
        from c:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/railties-4.1.8/lib/rails/commands/console.rb:9:in `start'
        from c:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/railties-4.1.8/lib/rails/commands/commands_tasks.rb:69:in `console'
        from c:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/railties-4.1.8/lib/rails/commands/commands_tasks.rb:40:in `run_command!'
        from c:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/railties-4.1.8/lib/rails/commands.rb:17:in `<top (required)>'
        from bin/rails:4:in `require'
        from bin/rails:4:in `<main>'
2

There are 2 answers

1
Shadwell On BEST ANSWER

I think you need to include a source: configuration option on your has_many through: associations. (You've correctly used class_name to identify the class at the other end but you need a little more than that).

From the rails guide on associations:

The :source option specifies the source association name for a has_many :through association. You only need to use this option if the name of the source association cannot be automatically inferred from the association name.

Because your associations are called marriages, invitations and ceremonies the has_many through: is looking for associations on WeddingAttendance called marriage, invitation and ceremony to follow to get the Wedding instance at the other end of the association.

If you add a source: :wedding to your through associations on User and a source: :user to your through associations on Wedding active record will be able to identify the associations it should follow.

0
AbM On

As @Shadwell mentioned, you need to use source with has_many :through.

However I would just create my own method:

class User

  has_many  :wedding_attendances
  has_many  :weddings,
          through: :wedding_attendances

  def self.marriages
    self.weddings.where('wedding_attendances.role = ?', WeddingAttendance.find_by_role(:spouse))
  end
end

This is just a preference but I find it is easier to read.