Active Record includes with STI
Asked Answered
B

3

6

I have the following model

class Event < ActiveRecord::Base
  has_many :attendances

class Attendance < ActiveRecord::Base
 belongs_to :user

class Student < User
  has_one  :student_detail

class StudentDetail < ActiveRecord::Base
  belongs_to :school

class Staff < User
  has_one :staff_detail

class StaffDetail < ActiveRecord::Base

The StudentDetail and StaffDetails have additional information, I am trying to avoid having it all in one STI user table due to having to work with something similar to concrete class per table pattern

I can do this easily enough

Event.includes(:attendances => :user).where(...)

but I want to be able to includes depending on user type e.g.

Event.includes(attendances: {:user => :student_details })

This will fail as some of the users are Staff objects.

I realise rails won't support this out of the box, but anyone have any tricks to get this to work

best solution right now would be split user on attendance to student and staff i.e.

class Attendance < ActiveRecord::Base
  belongs_to :student, -> {includes(:staff_detail) }
  belongs_to :staff, -> {includes(:student_detail) }
  #belong_to :user

which isn't ideal. Anyone have any tips? way to solve this.

Brand answered 6/1, 2015 at 2:34 Comment(0)
S
7

The easiest way is to just move the has_one associations down on to user. Since only Staff records will have staff_details, the preloading will just work.

class User < ActiveRecord::Base
  has_one :staff_detail
  has_one :student_detail
end

class Staff < User; end
class Student < User; end

That's not ideal though. To customise preloading further, you can use the Preloader class in Rails. First, load all the records without any includes, then iterate over them and preload the associations you need:

events = Event.includes(:attendances => :user)
users = events.users.flatten
users.group_by(&:class).each do |klass, records|
  associations = {
    Staff:   [:staff_detail],
    Student: [:student_detail]
  }.fetch(klass, [])

  ActiveRecord::Associations::Preloader.new(records, associations).run
end

Note that this API changed in Rails 4. In versions 3 and earlier, you just used the preload_associations method.

A while back I wrote a blog post about this same problem which includes a couple of other neat tricks (such as spec'ing that you get correct behaviour).

Stoic answered 19/1, 2015 at 18:54 Comment(1)
The syntax changed again, for Rails 4.1 I beleive. The new syntax is ActiveRecord::Associations::Preloader.new.preload(records, associations). See commit for details.Slesvig
A
2

How about putting the includes on the STI models as a default_scope?

class Event < ActiveRecord::Base
  has_many :attendances

class Attendance < ActiveRecord::Base
 belongs_to :user

class Student < User
  has_one  :student_detail
  default_scope includes(:student_detail)

class StudentDetail < ActiveRecord::Base
  belongs_to :school

class Staff < User
  has_one :staff_detail
  default_scope includes(:staff_detail)

class StaffDetail < ActiveRecord::Base

Then I think this:

Event.includes(:attendances => :user).where(...)

Should eager load for both Students and Staff.

Amedeo answered 12/1, 2015 at 23:48 Comment(0)
F
1

You could just use named scopes to make your life a little easier.

class Event < ActiveRecord::Base
  has_many :attendances
  scope :for_students, -> { includes(:attendances => { :users => :student_detail }).where('users.type = ?', 'Student') }
  scope :for_staff, -> { includes(:attendances => { :users => :staff_detail }).where('users.type = ?', 'Staff') }
end

Then you can just do Event.for_students

Familiarity answered 19/1, 2015 at 21:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.