Rails: Polymorphic model, troubles with deletion
In my Rails movie database, I have now added a "Tag" model that can tag both movies and actors. However I am not able to delete a tag that is used to tag either one of a movie or an actor (foreign_key constraint); when I call destroy on such a tag, Rails says:
Mysql2::Error: Cannot delete or update a parent row: a foreign key constraint fails (`newmovie_development`.`taggings`, CONSTRAINT `fk_rails_e21d88485b` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`))
Movie (actor is exactly the same in this regard):
class Movie < ApplicationRecord
...
has_many :taggings, as: :taggable, dependent: :destroy
has_many :tags, through: :taggings
...
Tag:
class Tag < ApplicationRecord
validates :name, presence: true
has_many :taggings
has_many :movies, through: :taggings, :source => 'taggable', :source_type => 'Movie'
has_many :actors, through: :taggings, :source => 'taggable', :source_type => 'Actor'
end
Tagging:
class Tagging < ApplicationRecord
belongs_to :tag
belongs_to :taggable, polymorphic: true
end
You're probably a bit confused here. If you have a movie or actor that you want to be able to edit the tags for you can use the tags_ids=
setter generated by has_many :tags
.
<%= form_with(model: @actor) do |form| %>
<div class="field">
<%= form.label :tags_ids, 'Tags' %>
<%= form.collection_checkboxes :tag_ids, Tag.all, :id, :name %>
</div>
<% end %>
class ActorsController < ApplicationController
before_action :set_actor, except: [:new, :create, :index]
def update
if @actor.update(actor_params)
redirect_to @actor, success: 'Actor updated'
else
render :new
end
end
private
def set_actor
@actor = Actor.find(params[:id])
end
def actor_params
params.require(:actor)
.permit(:foo, :bar, tag_ids: [])
end
end
This will automatically diff the array passed in the parameters against any existing rows in the taggings table and add/create rows accordingly.
If you want to create a button to "remove tags from a taggable" one by one (perhaps enhanced with ajax) you want to make sure you're actually deleting rows from the taggings table and not tags:
module Actors
class TagsController < ApplicationController
# DELETE /actors/1/tags/2
def destroy
@tagging = @actor.taggings.find_by!(tag_id: params[:id])
@tagging.destroy
end
private
def set_actor
@actor = Actor.find(params[:actor_id])
end
end
end
If you do actually want to remove a tag completly from the entire system (the equivilent of burnination here on Stackoverflow) you need to setup the dependent option on the assocation so that the foreign key constraint is not violated:
class Tag < ApplicationRecord
validates :name, presence: true
has_many :taggings, dependent: :destroy
# ...
end
You should also make sure to add a unique index to ensure that you don't get duplicates in your taggings table:
add_index :taggings, [:tag_id, :taggable_id, :taggable_type], unique: true
And a validation:
class Tagging
validates_uniqueness_of :tag_id, scope: [:taggable_id, :taggable_type]
end