はじめに
ブログ記事を投稿するアプリケーションで記事を投稿する際にタグ付けする機能を実装してみました。
Rails 初心者が出来る限り自力で実装してみました。おかしい点や改善点などあればコメントで指摘していただけると嬉しいです。
Rails : 6.0.2
Ruby : 2.7.0
仕様
- 記事投稿フォームにユーザーが任意のタグを入力してもらう
- タグは複数設定できる
- タグを一意(ユニーク)に保ちたい
- タグをアルファベットの大文字・小文字で区別させたくないので、タグが保存される前にアルファベットを全て小文字に変換して保存する
- タグ DB に既に存在する場合と存在しない場合で処理を分ける
- タグが既に存在する場合は、タグを DB から取得して紐付ける
- タグ名が DB に存在しない場合は、タグを作成して紐付ける
手順
- 各モデル(Article, Tag, 中間モデル)を作成する
- Articles コントローラを作成
- アソシエーションの設定
- ルーティングを設定
- Articles コントローラを編集
- Article モデルに save_tags()メソッドを定義
- view を編集
1. 各モデルを作成
ブログ記事(Article)タグ(Tag)中間テーブル(ArticleTagRelation)の各モデルを作成します。
$ rails g model Article title:string content:text
$ rails g model Tag tag_name:string
$ rails g model ArticleTagRelation article:references tag:references
rails g model Tag tag_name:string
で生成されたマイグレーションファイルを編集します。
class CreateTags < ActiveRecord::Migration[5.2]
def change
create_table :tags do |t|
# NULL での保存を DB レベルで制限
t.string :tag_name, null: false
t.timestamps
# tag_name で検索するため index を貼り 一意制約を設定
t.index :tag_name, unique: true
end
end
end
編集が完了したらマイグレートします。
$ rails db:migrate
2. Articles コントローラを作成
article コントローラを作成します。
$ rails g controller Articles new show
処理は後で作成します。
3. アソシエーションの設定
生成したそれぞれのモデルにアソシエーション(関連付け)を設定します。
- Articleモデル
class Article < ApplicationRecord
has_many :article_tag_relations, dependent: :destroy
has_many :tags, through: :article_tag_relations
end
- Tagモデル
class Tag < ApplicationRecord
has_many :article_tag_relations, dependent: :destroy
has_many :articles, through: :article_tag_relations
end
- ArticleTagRelationモデル
class ArticleTagRelation < ApplicationRecord
belongs_to :article
belongs_to :tag
end
今回は、Articles テーブルと Tags テーブルを ArticleTagRelations テーブルという中間テーブルで多対多のアソシエーションを設定します。
4. ルーティングを設定
一括で設定したいのでresources :articles
を設定します
- routes.rb
Rails.application.routes.draw do
resources :articles
end
5. Articles コントローラを編集
先ほど作成した Articles コントローラに処理を追加していきます
- articleコントローラ
class ArticlesController < ApplicationController
def new
@article = Article.new
end
def create
@article = Article.new(article_params)
if @article.save
# delete(" ")で文字列から全ての空白を削除する
# split(",")で受け取った文字列をカンマ(,)区切りで配列にする
tag_list = tag_params[:tag_names].delete(" ").split(",")
# Article.rb に save_tags()メソッドを定義
@article.save_tags(tag_list)
redirect_to @article
else
render 'new'
end
end
def show
@article = Article.find(params[:id])
end
private
def article_params
params.require(:article).permit(:title, :content)
end
# タグ用にストロングパラメータを設定して、文字列を受け取る
def tag_params
params.require(:article).permit(:tag_names)
end
end
tag_list = tag_params[:tag_names].delete(" ").split(",")
記事投稿フォームから送信されたタグ名(文字列)の空白を全て削除して、カンマ区切りで配列下しています。
配列にしている理由は、後ほど定義するsave_tags()
で複数のタグをそれぞれ ブログ記事にタグの作成・紐付けをするためです。
> "Ruby, Rails , Docker".delete(" ").split(",")
#=> ["Ruby","Rails","Docker"]
追記
tag_list = tag_params[:tag_names].delete(" ").split(",")
では、ユーザー側がカンマ区切りで入力する必要があるため、下記に修正
tag_list = tag_params[:tag_names].split(/[[:blank:]]+/).select(&:present?)
> "rails docker ruby".split(/[[:blank:]]+/).select(&:present?)
#=> ["rails", "docker", "ruby"]
split(/[[:blank:]]+/)
では、空白区切りで配列に分割してくれます。
split(" ")
でも半角スペース区切りで分割してくれるのですが、全角スペースなどが含まれてしまった場合は分割されないので、split(/[[:blank:]]+/)
を使うといいです。
# "ruby[半スペ]docker[全スペ][全スペ]rails"
> "ruby docker rails".split(" ")
#=> ["ruby", "docker", " ", "rails"]
`select(&:present?)`では、配列化した値をそれぞれ`present?`メソッドで判定して、真であれば取り出します。
※present?
メソッドは、Rails の拡張機能のため Rails 環境下のみ使用できます。
@article.save_tags(tag_list)
save_tags()
メソッドは、後ほど作成しながら説明していきます。
6. Article モデルに save_tags()メソッドを定義
ユーザーが入力したタグ名が既にDBに存在する場合は、DBから該当のデータを取得したり、DBに存在しない場合は新たに作成したいので、メソッドを定義してみます。
このメソッドは手探りで作成したため、改善点がありそう...
- Articleモデル
class Article < ApplicationRecord
has_many :article_tag_relations, dependent: :destroy
has_many :tags, through: :article_tag_relations
# articlesコントローラで配列化した値を引数で受け取ります
def save_tags(tag_list)
tag_list.each do |tag|
# 受け取った値を小文字に変換して、DBを検索して存在しない場合は
# find_tag に nil が代入され nil となるのでタグの作成が始まる
unless find_tag = Tag.find_by(tag_name: tag.downcase)
begin
# create メソッドでタグの作成
# create! としているのは、保存が成功しても失敗してもオブジェクト
# を返してしまうため、例外を発生させたい
self.tags.create!(tag_name: tag)
# 例外が発生すると rescue 内の処理が走り nil となるので
# 値は保存されないで次の処理に進む
rescue
nil
end
else
# DB にタグが存在した場合、中間テーブルにブログ記事とタグを紐付けている
ArticleTagRelation.create!(article_id: self.id, tag_id: find_tag.id)
end
end
end
end
Articles コントローラでユーザーから受け取った文字列を配列化して、引数として受け取ります。受け取った配列をeach
で回してそれぞれのタグ名が既にDBに存在するかをチェックし、存在すれば取得、存在しなければ作成しています。
find_by(tag_name: tag.downcase)
でdowncase
としているのは、タグを保存する直前で全て小文字にする予定なので、DB内を検索する際も小文字に変換してから検索しています。
begin
とrescue
の間でself.tags.create!(tag_name: tag)
としているのは、もしバリデーションエラーなどで保存が失敗した時、例外処理を返してrescue
内のnil
を返すことで保存されず次の処理が実行されるようにしています。
タグが保存される前に全て小文字に変換
"Rails".downcase == "rails"
としたいのでタグを保存する直前で全て小文字に変換してから保存するようにbefore_save
を使っていきます。
- Tagモデル
class Tag < ApplicationRecord
# 保存される直前に実行される
before_save :downcase_tag_name
has_many :article_tag_relations, dependent: :destroy
has_many :articles, through: :article_tag_relations
validates :tag_name, presence: true, uniqueness: true, length: { maximum: 50 }
private
# タグ名を小文字に変換
def downcase_tag_name
self.tag_name.downcase!
end
end
7. view を編集
2.のAricles コントローラの作成で同時に生成された new.html.erb と show.html.erb を編集していきます
記事やタグの値を送信するフォーム(new.html.erb)と作成した記事を確認する記事詳細(show.html.erb)となります
- new.html.erb
<h1>ブログ投稿画面</h1>
<%= form_with model: @article, local: true do |f| %>
<p><%= f.label :title, "タイトル" %></p>
<p><%= f.text_field :title %></p>
<p><%= f.label :tag_names, "タグ" %></p>
<p><%= f.text_field :tag_names %></p>
<p><%= f.label :content, "本文" %></p>
<p><%= f.text_area :content %></p>
<%= f.submit '投稿' %>
<% end %>
- show.html.erb
<h2>タイトル : <%= @article.title %></h2>
<% @article.tags.each do |tag| %>
<p>#<%= tag.tag_name %></p>
<% end %>
<h3>本文</h3>
<hr>
<p><%= @article.content %></p>
CSS ゼロなので見づらいかもしれませんが、確認用なのでよしとします。
次は紐付けたタグを使って検索をしたい
次はブログ記事とタグを作成・紐付けが完成したのでタグを使った検索機能を実装する予定なので、検索機能が実装できた時に更新したいと思います。