#98 Polymorphic Associations
9-17-2017

Summary

Advancing from Single Table Inheritance, learn how Polymorphic Associations differ and tricks to simplify their usage.
7
rails model 10:31

Summary

Terminalrails g scaffold company name website
rails g scaffold employees first_name last_name email birth_date:date
rails g model note notable:references{polymorphic} content:text
database migrationsclass CreateNotes < ActiveRecord::Migration[5.1]
  def change
    create_table :notes do |t|
      t.references :notable, polymorphic: true
      t.text :content

      t.timestamps
    end
  end
end

create_table "notes", force: :cascade do |t|
  t.string "notable_type"
  t.integer "notable_id"
  t.text "content"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["notable_type", "notable_id"], name: "index_notes_on_notable_type_and_notable_id"
end
note.rbclass Note < ApplicationRecord
  belongs_to :notable, polymorphic: true
end
employee.rbclass Employee < ApplicationRecord
  has_many :notes, as: :notable
end
company.rbclass Company < ApplicationRecord
  has_many :notes, as: :notable
end
routes.rbRails.application.routes.draw do
  resources :companies do
    resources :notes, module: :companies
  end

  resources :employees do
    resources :notes, module: :employees
  end

  ...
end
notes_controller.rbclass NotesController < ApplicationController
  def new
    @note = @notable.notes.new
  end

  def create
    @note = @notable.notes.new note_params
    @notable.save
    redirect_to @notable, notice: "Your note was successfully posted."
  end

  private

    def note_params
      params.require(:note).permit(:content)
    end
end
employees/notes_controller.rbclass Employees::NotesController < NotesController
  before_action :set_notable

  private

    def set_notable
      @notable = Employee.find(params[:employee_id])
    end
end
companies/notes_controller.rbclass Companies::NotesController < NotesController
  before_action :set_notable
  
  def create
    # NOTIFY
    super
  end

  private

    def set_notable
      @notable = Company.find(params[:company_id])
    end
end
employees/show.html.erb<%= render partial: "notes/notes", locals: {notable: @employee} %>
<%= render partial: "notes/form", locals: {notable: @employee} %>
companies/show.html.erb<%= render partial: "notes/notes", locals: {notable: @company} %>
<%= render partial: "notes/form", locals: {notable: @company} %>
notes/_form.html.erb<%= form_with(model: [notable, Note.new], local: true) do |form| %>
  <div class="field">
    <%= form.label :content %><br/>
    <%= form.text_area :content %>
  </div>
  <%= form.submit class: "btn btn-primary" %>
<% end %>
notes/_notes.html.erb<h3>Notes</h3>
<% notable.notes.each do |note| %>
  <p>
    <hr>
    <%= note.content %>
    <em><%= time_ago_in_words note.created_at %></em>
  </p>
<% end %>


hackvan said 4 months ago:

Excellent episode, very useful.

Wolfgang Barth PRO said 28 days ago:

I like it, routing via :modules is very elegant. It works for me in a normal rails application, but i can't get it work inside of a rails engine.

config/routes.rb from the engine:

Wobauth::Engine.routes.draw do
  resources :users do
    resources :authorities, module: :users
  end
end

Controllers:

module Wobauth
  class Users::AuthoritiesController < AuthoritiesController ...
  end
end
module Wobauth
  class AuthoritiesController < ApplicationController
   ...
  end
end

I get the following error:

uninitialized constant Authority
Extracted source (around line #269):
      names.inject(Object) do |constant, name|
        if constant == Object
          constant.const_get(name)
        else
          candidate = constant.const_get(name)
          next candidate if constant.const_defined?(name, false)

It works without specifying :modules, but this means i must place the logic in the main authorities_controller ... so its less elegant. Any idea?

Wolfgang.

kobaltz PRO said 28 days ago:

My guess would be when the type is saved to the database, it is being saved as Authority instead of Wobauth::Authority. Try adding a class_name to the association to see if that makes a difference. The users might be wobauth_users depending where it is declared.

has_many :users, class_name: 'Wobauth::Authority'

Wolfgang Barth PRO said 28 days ago:

Yeah, found it. The problem comes from cancancan/lib/cancan/controller_resource.rb in 


    def resource_class
      case @options[:class]
      when false
        name.to_sym
      when nil
        namespaced_name.to_s.camelize.constantize
      when String
        @options[:class].constantize
      else
        @options[:class]
      end
    end

The error cames from namespaced_name.to_s.camelize.constantize, which resolves to "Authority". I now set

module Wobauth
  class AuthoritiesController < ApplicationController
    skip_load_and_authorize_resource
    load_and_authorize_resource class: Wobauth::Authority ...

This sets the class name manually. Seems to work now. Thank you for the idea ;-)

Login to Comment