12

Is it possible for a model to belong_to, two models and have a nested relationship?

i.e of what i want

class trainer
has_many :appointments
end

class appointment
belong_to :trainer, :customer
end

class customer
has_many :appointments
end

at the moment i have only the customer and appointment models which are nested e.g of what i have:

create method looks like this:

  def create
    @appointment = @customer.appointments.build(params[:appointment])

    respond_to do |format|
      if @appointment.save
        format.html { redirect_to([@customer, @appointment], :notice => 'Appointment was successfully created.') }
        format.xml  { render :xml => @appointment, :status => :created, :location => @appointment }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @appointment.errors, :status => :unprocessable_entity }
      end
    end
  end

in routes i have:

  map.resources :patients, :has_many => [ :appointments, :visits ]

is it possible to have 2 nested relationships for 1 model? what would i have to change my create method to, if appointment also belonged to trainer as well as customer?

thanks

Mo.
  • 40,243
  • 37
  • 86
  • 131

1 Answers1

25

Assuming that you're using ActiveRecord: Having a model belong to more than one other model is possible of course (however you need to specify one belongs_to statement for each relation).

class Appointment < ActiveRecord::Base
  belongs_to :trainer
  belongs_to :customer
end

A belongs_to relation does not necessarily mean that the record actually has that other record related; it can also be nil. So you can have appointments that belong to a trainer but no customer and vice versa.

Actually you can even have neither a trainer nor a customer or both a trainer and a customer as well this way - if this violates your business logic, you might want to add a validation to prevent this.

Your existing controller create method should continue to work like it is, you just need to add the handling of trainer records. You can even use the same controller for handling appointment of trainers and customers by abstracting trainers and customers, e.g. into a person like this:

class AppointmentsController < ApplicationController

  def create
    @appointment = person.appointments.build(params[:appointment])
    # ...
  end

protected

  def person
    @person ||=
      if params[:trainer_id]
        Trainer.find(params[:trainer_id])
      elsif params[:customer_id]
        Customer.find(params[:customer_id])
      end
  end
end

This way, you can use the same AppointmentsController for both routes

# Use AppointmentsController for /trainers/123/appointments
# as well as for /customers/123/appointments
map.resources :trainers, :has_many => :appointments
map.resources :customers, :has_many => :appointments

Of course, this only makes sense if the logic and views behind trainer appointments and customer appointments are almost the same. If not, you can also use different controllers

# Use TrainerAppointmentsController for /trainers/123/appointments and
# CustomerAppointmentsController for /customers/123/appointments
map.resources :trainers do |trainer|
  trainer.resources :appointments, :controller => 'trainer_appointments'
end
map.resources :customers do |customer|
  customer.resources :appointments, :controller => 'customer_appointments'
end
Zargony
  • 9,615
  • 3
  • 44
  • 44
  • + 1 brilliant answer! thank you. just one question, if i create the appointment from the customer page/view. how do i add the trainer ID to the appointment? can i just load all trainers in the view and allow user to select a trainer from the drop down menu then assign the trainer ID to the trainer_id filed in the appointment? – Mo. Aug 04 '10 at 09:09
  • Is your question about the trainer_id param or about the record attribute? The trainer_id param gets set by the nested resource route. `/trainers/123/appointments` sets `params[:trainer_id]` to 123, `/customers/123/appointments` sets `params[:customer_id]` to 123. The trainer_id attribute on the appointments record gets set by the has_many association. `@appointment = person.appointments.build` will automatically set `@appointment.trainer_id` or `@appointment.customer_id` to `person.id` depending on what class person is (to change the defaults, there's a :foreign_key option for belongs_to). – Zargony Aug 04 '10 at 11:14
  • lets say i go on to a customer profile and create appointment from his profile (it will pick up the id from the customer_id params) but how do i then assign that appointment a trainer that will be in charge of that appointment. if i have 10 trainers, how do i set the appointment to one of them? i want to allow the user to pick from a drop down menu a trainer and then set the ID of that trainer to the trainer_id field? – Mo. Aug 04 '10 at 21:31
  • The easiest way would be, to have the appointment form submit a value for params[:appointment][:trainer_id] along with the other fields. The update action will then set the trainer_id attribute and the association gets saved. There are form helpers that create dropdown select boxes from a collection, so the user gets a nice trainer name selection. `collection_select` is probably the helper you're looking for to use in the view: http://apidock.com/rails/ActionView/Helpers/FormOptionsHelper/collection_select – Zargony Aug 04 '10 at 22:16
  • 1
    This is ages old but still comes up for 'two belongs to relationships' - so worth noting that since Rails 5, the `belongs_to` model is now required when saving the child instance; saving will fail without having the associated model. Can be ignored with `belongs_to :trainer optional: true` - https://github.com/rails/rails/pull/18937 – Jarvis Johnson Dec 17 '18 at 14:58