Active Record includes with STI - ruby-on-rails

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.

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

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.

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

Related

Is: grandparent.parents.children association chaining not correct in Rails 4?

I'm having trouble figuring out the proper way of retrieving all children of multiple parents through association chaining.
To simplify I have three models:
class Customer < ActiveRecord::Base
has_many :invoices
end
class Invoice < ActiveRecord::Base
belongs_to :customer
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :invoice
end
After creating a few objects I tired to use the example from rails guides (association basics: 4.3.3.4 includes):
Customer.first.invoices.line_items
It returns:
undefined method `line_items' for #<Customer::ActiveRecord_Associations_CollectionProxy
Is grandparent.parents.children not usable?
EDIT
I'm not searching for the grandparent.parents.first.children, but all children of all parents in the collection, rails guides state:
If you frequently retrieve line items directly from customers (#customer.orders.line_items),
As a valid operation, I would like to know if that is a mistake.
FINAL As stated in the comments of the selected answer: in ActiveRecord: scopes are chainable but associations are not.
The customer.invoices.line_items cannot work the way you want to, since the has_many always is linked to a single record. but you can achieve what you want (if I understand correctly) using has_many through
as follows:
class Customer < ActiveRecord::Base
has_many :invoices
has_many :line_items, through: :invoices
end
class Invoice < ActiveRecord::Base
belongs_to :customer
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :invoice
end
and now you can write:
customer.line_items
and it will return all line_items which are connected to a customer's invoices.
Customer.first.invoices.first.line_items
Or if you want all of the data together, you can do something like:
results = Customer.first.invoices.includes(:line_items)
Then you may access data with no DB call, by looping results. For first data ex: results.first.line_items
Hope it helps!
Customer.first.invoices will return an collection (like an array) of invoices. The line_items method isn't defined for a collection, but its defined for an invoice. Try Customer.first.invoices.first.line_items
EDIT - If you always want the orders to include the line items, you can just do:
class Customer < ActiveRecord::Base
has_many :orders, -> { includes :line_items }
end
class Order < ActiveRecord::Base
belongs_to :customer
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :order
end

has_and_belongs_to_many STI, restrict to be unique by types

I'm using STI models with has_and_belongs_to_many relations.
So I have User with many Templates of different types, like MainTemplate < Template; NotSoMainTemplate < Template; etc.
Is there a way to limit each user to have only one MainTemplate and only one NotSoMainTemplate, and so on for all types?
Let me reiterate the problem statement as I have understood it.
You want User to have at most one kind of each template. i.e.
1 MainTemplate, 1 NotSoMainTemplate, etc.
You don't need a direct relation with Template (parent table)
Each template may be used by more than one user
Based on the above assumption, I would suggest you to do the following:
Remove existing habtm association between User and Template
Add migrations to add main_template_id, not_so_main_template_id to User
Add the following associations:
class MainTemplate < Template
has_many :users
end
class NotSoMainTemplate < Templete
has_many :users
end
class class User < ActiveRecord::Base
belongs_to :main_template
belongs_to :not_so_main_template
end
Since you are already using STI, you can try has_one.
class Template < ActiveRecord::Base
has_many :users, through: template_choice
...
end
class MainTemplate < Template
...
end
class TemplateChoice < ActiveRecord::Base
belongs_to :template_choice
belongs_to :user
end
class User < ActiveRecord::Base
has_one :main_template, through: :template_choice
has_one :not_so_main_template, through: :template_choice
...
end

In Rails ActiveRecord, how can you create a scope that uses a join through another model (two inner joins)?

I have a system with instructors, services offered, and availabilities.
An instructor can offer one or more servies, and an availability is associated with an instructor.
I want to create a scope on availabilities that gets all availabilities of a service.
The classes:
class Instructor < ActiveRecord::Base
has_many :instructor_availabilities
has_and_belongs_to_many :services
end
class Service < ActiveRecord::Base
attr_accessible :name
has_and_belongs_to_many :instructors
end
class InstructorAvailabililty < ActiveRecord::Base
belongs_to :instructor
attr_accessible time
scope :of_service, lambda { |service_id|
#code goes here for this scope
}
end
How would I implement this of_service scope, using two inner joins to get availabilities of a service (by going through the instructors to services mapping)?
Figured it out:
class Scheduling::InstructorAvailability < ActiveRecord::Base
belongs_to :instructor
attr_accessible time
joins(:instructor => :services).where('scheduling_instructors_services.service_id' => service_id)
}
end

Can a 3-way relationship be modeled this way in Rails?

A User can have many roles, but only one role per Brand.
Class User < AR::Base
has_and_belongs_to_many :roles, :join_table => "user_brand_roles"
has_and_belongs_to_many :brands, :join_table => "user_brand_roles"
end
The problem with this setup is, how do I check the brand and the role at the same time?
Or would I better off with a BrandRole model where different roles can be set up for each Brand, and then be able to assign a user to a BrandRole?
Class User < AR::Base
has_many :user_brand_roles
has_many :brand_roles, :through => :user_brand_roles
end
Class BrandRole < AR::Base
belongs_to :brand
belongs_to :role
end
Class UserBrandRole < AR::Base
belongs_to :brand_role
belongs_to :user
end
This way I could do a find on the brand for the user:
br = current_user.brand_roles.where(:brand_id => #brand.id).includes(:brand_role)
if br.blank? or br.role != ADMIN
# reject access, redirect
end
This is a new application and I'm trying to learn from past mistakes and stick to the Rails Way. Am I making any bad assumptions or design decisions here?
Assuming Roles,Brands are reference tables. You can have a single association table Responsibilities with columns user_id, role_id, brand_id.
Then you can define
Class User < AR::Base
has_many : responsibilities
has_many :roles, :through => responsibilities
has_many :brands,:through => responsibilities
end
Class Responsibility < AR::Base
belongs_to :user
has_one :role
has_one :brand
end
The you can define
Class User < AR::Base
def has_access?(brand)
responsibility = responsibilities.where(:brand => brand)
responsibility and responsibility.role == ADMIN
end
end
[Not sure if Responsibility is the term used in your domain, but use a domain term instead of calling it as user_brand_role]
This is a conceptual thing. If BrandRole is an entity for your application, then your approach should work. If BrandRole is not an entity by itself in your app, then maybe you can create a UserBrandRole model:
class User < AR::Base
has_many :user_brand_roles
end
class Brand < AR::Base
has_many :user_brand_roles
end
class Role < AR::Base
has_many :user_brand_roles
end
class UserBrandRole < AR::Base
belongs_to :user
belongs_to :brand
belongs_to :role
validates_uniqueness_of :role_id, :scope => [:user_id, :brand_id]
end

Relation between two single-table inheritance models

I have the following two models
class ContactField < ActiveRecord::Base
end
class Address < ContactField
end
class Phone < ContactField
end
and
class Contact < ActiveRecord::Base
end
class Company < Contact
end
class Person < Contact
end
I want one contact, no matter is it Company or Person, to have many ContactFields(Addresses and Phones)... So where should I put those has many and belongs to?
Thanks
You already said it in plain english :-)
I want one contact, no matter is it Company or Person, to have many ContactFields(Addresses and Phones)... So where should I put those has many and belongs to? Thanks
class Contact < ActiveRecord::Base
has_many :contact_fields
end
class ContactField < ActiveRecord::Base
belongs_to :contact
end
This Relationship will be inherited by both address and phone
Looks like you're describing a belongs to relationship. The associations should be defined in the parent class, so they can be inherited by the subclasses.
class ContactField < ActiveRecord::Base
belongs_to :contact
belongs_to :company, :foreign_key => :contact_id
belongs_to :person, :foreign_key => :contact_id
end
class Contact < ActiveRecord::Base
has_many :contact_fields
has_many :addresses
has_many :phones
end
However #contact.contact_fields will just return ContactField records. If you need the methods defined in any of the sub classes you can always use the becomes method. There are a few ways around that. Such adding the extra associations, like I did. Or using ActiveRecord::Base#becomes

Resources