Form not working for polymorphic has_many through association
In my Rails 5.1 app I am trying to create a tagging system from scratch. I have been using the Comments with Polymorphic Associations episode as a template.
I want Tags
to be a polymorphic has_many :through
association so that I can tag multiple models.
Currently I'm able to create a Tag
(and the associated Tagging
) in the console by doing: Note.last.tags.create(name: "example")
which generates the correct SQL:
Note Load (0.2ms) SELECT "notes".* FROM "notes" ORDER BY "notes"."id" DESC LIMIT $1 [["LIMIT", 1]]
(0.2ms) BEGIN
SQL (0.4ms) INSERT INTO "tags" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "example"], ["created_at", "2017-10-21 14:41:43.961516"], ["updated_at", "2017-10-21 14:41:43.961516"]]
Note Load (0.3ms) SELECT "notes".* FROM "notes" WHERE "notes"."id" = $1 LIMIT $2 [["id", 4], ["LIMIT", 1]]
SQL (0.4ms) INSERT INTO "taggings" ("created_at", "updated_at", "tag_id", "taggable_id", "taggable_type") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["created_at", "2017-10-21 14:41:43.978286"], ["updated_at", "2017-10-21 14:41:43.978286"], ["tag_id", 9], ["taggable_id", 4], ["taggable_type", "Note"]]
But when trying to create a Tag
and its associations through my form it doesn't work. I can create the Tag
but no Tagging
.
controllers/notes/tags_controller.rb
class Notes::TagsController < TagsController
before_action :set_taggable
private
def set_taggable
@taggable = Note.find(params[:note_id])
end
end
controllers/tags_controller.rb
class TagsController < ApplicationController
before_action :authenticate_user!
def create
@tag = @taggable.tags.new(tag_params)
@tag.user_id = current_user.id
if @tag.save
redirect_to @taggable, success: "New tag created."
else
render :new
end
end
private
def tag_params
params.require(:tag).permit(:name)
end
end
routes.rb
...
resources :notes, except: [:index] do
resources :tags, module: :notes
end
...
.
class Note < ApplicationRecord
belongs_to :notable, polymorphic: true
has_many :taggings, as: :taggable
has_many :tags, through: :taggings
end
class Tag < ApplicationRecord
has_many :taggings
has_many :taggables, through: :taggings
end
class Tagging < ApplicationRecord
belongs_to :tag
belongs_to :taggable, polymorphic: true
end
notes/show.html.erb
<p><%= @note.body %></p>
<%= render partial: 'tags/tags', locals: { taggable: @note } %>
<%= render partial: 'tags/form', locals: { taggable: @note } %>
tags/form.html.erb
<%= simple_form_for [taggable, Tag.new] do |f| %>
<%= f.input :name %>
<%= f.submit %>
<% end %>
So Rails has some interesting behaviors to learn when saving models with join models. Most noteably you can save the join by adding the secondary child and passing it to the array on the parent. For example:
note = Note.first
tag = Tag.create(name: 'foo')
note.tags << tag
Or you can do what the module does below here and build up all the objects and save them in a transaction (for proper rollback behavior).
module Taggable
extends ActiveSupport::Concern
included do
has_many :taggings, as: :taggable
has_many :tags, through: :taggings
end
def tag_list
tags.pluck(:name)
end
def add_tag(tag_name)
self.class.transaction do
tagging = taggings.new
tagging.tags.new(name: tag_name)
tagging.save
end
end
end
class Note < ApplicationRecord
include Taggable
belongs_to :notable, polymorphic: true
end
class TagsController < ApplicationController
before_action :authenticate_user!
def create
if @taggable.add_tag(tag_params[:name])
redirect_to @taggable, success: "New tag created."
else
render :new
end
end
private
def tag_params
params.require(:tag).permit(:name)
end
end