Reputation: 969
I'm trying to add existing Tags to Articles.
My models:
# app/models/user.rb
class User < ApplicationRecord
has_many :articles, dependent: :destroy
has_many :tags, dependent: :destroy
end
# app/models/article.rb
class Article < ApplicationRecord
belongs_to :user
has_many :tags
end
# app/models/tag.rb
class Tag < ApplicationRecord
belongs_to :user
belongs_to :article
has_many :articles
validates :name, presence: true, uniqueness: { scope: :user_id }
end
By using the following in the console, I'm able to add a tag to an article.
> a = Article.last
> a.tags.create(name: "unread", user_id: "1")
> a.tags
Tag Load (0.3ms) SELECT "tags".* FROM "tags" WHERE "tags"."article_id" = ? [["article_id", 29]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Tag id: 6, name: "unread", user_id: 1, article_id: 29, created_at: "2020-12-28 16:05:36", updated_at: "2020-12-28 16:05:36", permalink: "unread">]>
If I try to add this same tag to different article using the same .create
, I get a rollback error.
> a = Article.first
> a.tags.create(name: "unread", user_id: "1")
(0.0ms) begin transaction
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Tag Exists? (0.1ms) SELECT 1 AS one FROM "tags" WHERE "tags"."name" = ? AND "tags"."user_id" = ? LIMIT ? [["name", "unread"], ["user_id", 1], ["LIMIT", 1]]
(0.0ms) rollback transaction
Thinking the problem is the .create
since that tag already exists, I tried this first_or_create
, but that also errored.
> a.tags.first_or_create(name: "unread", user_id: "1")
Tag Load (0.1ms) SELECT "tags".* FROM "tags" WHERE "tags"."article_id" = ? ORDER BY "tags"."id" ASC LIMIT ? [["article_id", 52], ["LIMIT", 1]]
(0.0ms) begin transaction
Tag Exists? (0.1ms) SELECT 1 AS one FROM "tags" WHERE "tags"."name" = ? AND "tags"."user_id" = ? LIMIT ? [["name", "unread"], ["user_id", 1], ["LIMIT", 1]]
(0.3ms) rollback transaction
How do I add an existing tag to an article?
Edit:
I've added a join table as prescribed by Max. This has allowed me to save tags to articles via the console.
> t = Tag.where(name: "rails", user_id: "1").first_or_create
Tag Load (0.6ms) SELECT "tags".* FROM "tags" WHERE "tags"."name" = ? AND "tags"."user_id" = ? ORDER BY "tags"."id" ASC LIMIT ? [["name", "rails"], ["user_id", 1], ["LIMIT", 1]]
=> #<Tag id: 8, name: "rails", user_id: 1, created_at: "2020-12-28 21:21:25", updated_at: "2020-12-28 21:21:25", permalink: "rails">
> a.tags << t
(0.0ms) begin transaction
Article Tag Exists? (0.1ms) SELECT 1 AS one FROM "article_tags" WHERE "article_tags"."tag_id" = ? AND "article_tags"."article_id" = ? LIMIT ? [["tag_id", 8], ["article_id", 29], ["LIMIT", 1]]
Article Tag Create (0.2ms) INSERT INTO "article_tags" ("article_id", "tag_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["article_id", 29], ["tag_id", 8], ["created_at", "2020-12-28 21:22:13.567187"], ["updated_at", "2020-12-28 21:22:13.567187"]]
(17.9ms) commit transaction
Tag Load (0.1ms) SELECT "tags".* FROM "tags" INNER JOIN "article_tags" ON "tags"."id" = "article_tags"."tag_id" WHERE "article_tags"."article_id" = ? LIMIT ? [["article_id", 29], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Tag id: 6, name: "unread", user_id: 1, created_at: "2020-12-28 16:05:36", updated_at: "2020-12-28 16:05:36", permalink: "unread">, #<Tag id: 8, name: "rails", user_id: 1, created_at: "2020-12-28 21:21:25", updated_at: "2020-12-28 21:21:25", permalink: "rails">]>
I'm still unclear how to save this from the view.
I have a form in app/views/articles/edit.html.erb:
<%= form_with(model: @article, local: true) do |form| %>
<%= form.label :tag_list, "Tags, separated by comma" %>
<%= form.text_field :tag_list %>
<%= form.submit "Update Tags" %>
<% end %>
In the Articles Controller I assume I'd want the following.
def edit
@article.tags << Tag.where(name: "unread", user_id: "1").first_or_create
@article.save!
end
...or perhaps I need to loop those tags in the model? Something like this:
# app/models/article.rb
def tag_list
tags.map(&:name).join(", ")
end
def tag_list=(names)
self.tags = names.split(",").map do |name|
Tag.where(name: name.strip, user_id: current_user.id).first_or_create!
end
end
# app/controllers/articles_controller.rb
def edit
@article.tags << tag_list
@article.save!
end
But this isn't working. How do I update the tags from a view?
Edit:
I've decided not to update tags by form, so I've marked Max's answer as the solution since he pointed me to the Join Tables which got me on the right track.
If you wish to update this for posterity, I'm sure they will appreciate it.
Upvotes: 0
Views: 91
Reputation: 102036
If you want a tag to ever belong to more then a single article you need a join table:
class Article < ApplicationRecord
has_many :article_tags
has_many :tags, through: :article_tags
end
# rails g model article_tag article:references tag:references
# make sure to add a unique index on article_id and tag_id
class ArticleTag < ApplicationRecord
belongs_to :article
belongs_to :tag
validates_uniqueness_of :tag_id, scope: :article_id
end
class Tag < ApplicationRecord
has_many :article_tags
has_many :articles, through: :article_tags
end
This creates a many to many association between the two tables (an article can have multiple tags, and a tag can belong to multiple articles). This can also be done through has_and_belongs_to_many
which does not have a join model but has limited flexibility.
If you want to add an existing tag(s) to an article you can use the shovel operator:
a = Article.last
a.tags << Tag.first
a.tags << Tag.second
But usually you use the tag_ids=
setter to set the association from an array of ids:
class ArticlesController
def create
@article = Article.new(article_params)
# ...
end
private
def article_params
require(:article)
.permit(:title, :body, tag_ids: [])
end
end
<%= form_with(model: @article) do |f| %>
# ...
<%= f.collection_checkboxes :tag_ids, Tag.all, :id, :name %>
<% end %>
tag_ids: []
permits an array of ids.
This will automatically add/remove rows in the join table depending on what the user checks.
Upvotes: 1