Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Active Record includes with STI

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.

like image 446
dboyd68 Avatar asked Jan 06 '15 02:01

dboyd68


1 Answers

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).

like image 103
Xavier Shay Avatar answered Oct 24 '22 21:10

Xavier Shay