はじめに
タグ機能をgemを使わず、実装するための忘備録です。
設計は、中間テーブルを使うToxi法、といわれる形式で作成します。
実行環境
- Rails 7.0.4.1
- Ruby 3.0.4
- Devise 4.8.1
実装
1. アソシエーション
articlesテーブルとtagsテーブルを多対多でつなげます。
こうすることで、tagの編集をしたいときに、それぞれのarticleを編集する必要がなくなります。
注意する点は、articlesテーブルとtagsテーブルは親子関係にはなっていないことです。
そのため、modelに dependent: :destroyとつけても articleの削除と同時に、tagは削除されません。
構造を考えれば、当たり前なのですが、articleを削除してもtagが消えず、理解するまでに時間がかかりました。
また、中間テーブルの命名は、つなげるテーブルの名前 + Relationships (TagRelationships) のようにしているものが多く見られました。
今回は後で、タグとは別にカテゴリー機能を追加する際にも同じテーブルを使いたいと思い、ArticleRelationshipsとしました。
追記:
交差テーブルには関連の意味を表す名前をつけた方がいいようなので今回の場合、Taggingsとする方が良いようです。
2. モデル
2.1 モデルの作成
rails g model tag name:string
rails g model article_relationship article:references tag:references
マイグレーションファイルに追記をします。
class CreateTags < ActiveRecord::Migration[7.0]
def change
create_table :tags do |t|
t.string :name, null: false
t.timestamps
end
add_index :tags, :name, unique: true
end
end
modelのバリデーションで、presense: trueとすると、
railsから実行にはnullが使えなくなりますが、まだ、SQLの実行ではnullで保存ができてしまうようです。
そこで、migrateで null: false とすることで、SQLからの実行でもnullの保存ができなくなります。また、このテーブルはタグのidと名前があるだけなので、indexをつける必要はないでしょう。
indexは検索時間を高速化するためるためだけではなく、重複を防ぐためにも使うため、今回はindexを書く必要があります。
class CreateArticleRelationships < ActiveRecord::Migration[7.0]
def change
create_table :article_relationships do |t|
t.references :article, null: false, foreign_key: true
t.references :tag, null: false, foreign_key: true
t.timestamps
end
add_index :article_relationships, [:article_id, :tag_id], unique: true
end
end
unique: true としたことで、同じ名前のタグの保存をできないようにしています。
データベースへ反映させます。
rails db:migrate
2.2 モデルの関連付け
class Tag < ApplicationRecord
has_many :article_relationships, dependent: :destroy
has_many :articles, through: :article_relationships
validates :name, presence: true, uniqueness: true
end
class ArticleRelationship < ApplicationRecord
belongs_to :article
belongs_to :tag
validates :tag_id, uniqueness: { scope: :article_id }
end
class Article < ApplicationRecord
.
.
has_many :article_relationships, dependent: :destroy
has_many :tags, through: :article_relationships
.
.
end
validates :name, uniqueness: trueでタグの名前が重複する保存を防ぎます。
3. Viewの作成
<%= form_with(model: @article) do |f| %>
.
<%= f.text_field :tag_ids, class: "form-control", id:'tag_ids',\
placeholder: "タグをつける。複数つけるには','で区切ってください。" %>
.
<% end %>
<% article.tags.each do |tag| %>
<%= tag.name %>
<% end %>
4. コントローラーの作成
def save_tags(savearticle_tags)
savearticle_tags.each do |new_name|
article_tag = Tag.find_or_create_by(name: new_name)
self.tags << article_tag
end
end
タグを追加できるようにします。
「find_or_create_by」メソッドはカラムの中から同じ値がないか探して、あればそのままfindの動き、なければcreateの動きで新たにカラムに保存します。
def create
@article = current_user.articles.build(article_params)
tag_list = params[:article][:tag_ids].split(',') #追記
@article.image.attach(params[:article][:image])
if @article.save
@article.save_tags(tag_list) #追記
flash[:success] = "Article created!"
redirect_to root_url
else
render 'new', status: :unprocessable_entity
end
end
tag_listとして、タグ一覧を取得します。複数タグが入力されている場合、文字列として送られてくるタグをsplit(',')で分割して配列にします。
@aritcle.save_tags(tag_list)でタグをarticleに関連づけて保存します。
5. タグ一覧ページを作る
これで、タグの追加ができるようになりました。
ここからは、追加されたタグの一覧を表示し、そのタグをもつ記事に飛べるようにしたいと思います。
5.1 アクションを作成
class ArticlesController < ApplicationController
.
.
def index
@articles = params[:tag_id].present? ? Tag.find(params[:tag_id]).articles : Article.all
end
.
.
def tags
@tags = Tag.all
end
.
end
@articles = params[:tag_id].present? ? Tag.find(params[:tag_id]).articles : Article.all は、
params[:tag_id]があるかを確認し、
あればtag_idを使ってタグを検索して指定のタグをもつ記事を、なければ、全ての記事を@articleへ格納しています。
このコードにより、 タグ一覧ページ作成に必要なことはほとんど可能になっているので、あとはルーティングとviewを変更するだけです。
5.1 ルーティングとビュー
resources :articles do
get :tags, on: :collection
end
:collectionとルーティングを行うことで、/articles/tagsでタグの一覧を取得することができます。
<% provide(:title, "記事一覧") %>
<div>
<h2>記事一覧</h2>
<% @articles.each do |article| %>
<%= article.title %><br>
<% end %>
</div>
ここの@ariticlesは先ほど宣言したものです。
/articlesへ直接アクセスすると、@articlesにはparams[:tag_id]がないため、全てのariticleが表示されます。
<% provide(:title, "タグ一覧") %>
<div>
<h2>タグ一覧</h2>
<% @tags.each do |t| %>
<%= link_to t.name, articles_path(tag_id: t.id)%>
<% end %>
</div>
/article/tags にはタグの名前をさせ、tag_idをつかって、指定したariricleのページに飛ぶようにしています。
以上で、タグ一覧ページ完成となります。
参考にさせていただいた記事
https://qiita.com/you8/items/b2394104c6f9865f5d46
https://qiita.com/E6YOteYPzmFGfOD/items/bfffe8c3b31555acd51d
https://qiita.com/E6YOteYPzmFGfOD/items/177f18e706df05f9b42e