やりたいこと
①投稿に対して複数タグを付けれるようにする ②タグをリンクにして、検索できるようにする。リンクになってるタグを押すと、 そのタグを付与してる記事一覧が表示される複数タグの投稿
投稿一覧画面でタグの一覧と、記事数の表示
タグを押すと、検索ができ、そのタグがついてる投稿を検索できる
投稿詳細画面
タグを一個だけ付与するのは、別記事で投稿しました。
複数タグの実装はかなり難しかったのでまとめていきます。
①テーブル定義
一応、以下の感じです。
タグ:投稿=1:N(複数)
投稿:タグ=1:N(複数) の多 対 多 になるので、
中間テーブルであるPostTagを用意して、それぞれの外部キーを持たせてます。
①モデル/マイグレーションファイル作成
モデル作成
rails g model Tag name:string
rails g model PostTag post:references tag:references
マイグレーションファイル
class CreateTags < ActiveRecord::Migration[5.2]
def change
create_table :tags do |t|
t.string :name,null: false
t.timestamps
end
end
end
class CreatePostTags < ActiveRecord::Migration[5.2]
def change
create_table :post_tags do |t|
t.references :post, foreign_key: true
t.references :tag, foreign_key: true
t.timestamps
end
# 同じタグを2回保存するのは出来ないようになる
add_index :post_tags, [:post_id, :tag_id], unique: true
end
end
add_index :post_tags, [:post_id, :tag_id], unique: true
これは、複合キーインデックスで、この記述で同じタグを2回保存できないようにしています。
ここで、rails db:migrate
して、続いてモデルファイルを記載します。
モデルファイル
class Tag < ApplicationRecord
has_many :post_tags,dependent: :destroy, foreign_key: 'tag_id'
# タグは複数の投稿を持つ それは、post_tagsを通じて参照できる
has_many :posts,through: :post_tags
validates :name, uniqueness: true, presence: true
end
初めてthrough〜
を見たときはなんのこっちゃでしたが、、だいぶ理解できるようになってきました!
中間テーブルは、”仲介役”という風に考えるようにしてから、何だかわかるようになってきた気がします。
class PostTag < ApplicationRecord
belongs_to :post
belongs_to :tag
validates :post_id, presence: true
validates :tag_id, presence: true
end
validates〜_のところはPostとTagの関係を構築する際、2つの外部キーが存在することは絶対なので、
バリデーションを貼るそうですが、無くても問題ない気がしています(by初心者)
これまでの中間テーブルでもvalidationつけてないので。
# タグのリレーションのみ記載
has_many :post_tags,dependent: :destroy
has_many :tags,through: :post_tags
ここからが難しかった・・
何書いてあるかわからないので1行ずつ見ていきます。①コントローラー記述
```rb:post_controller.rb def create @post = Post.new(post_params) @post.user_id=current_user.id # 受け取った値を,で区切って配列にする tag_list=params[:post][:name].split(',') if @post.save @post.save_tag(tag_list) redirect_to posts_path(@post),notice:'投稿完了しました:)' else render:new end ```tag_list=params[:post][:name].split(',')
なんかpaizaで勉強した記憶があるsplit..確か分割して配列を作ってくれるようやつだったように思います。
「splitとは」
1番目の引数に指定したパターンに従って文字列を分割し、分割された各部分文字列を要素とする配列を取得します。
@post.save_tag(tag_list)
save_tagなんてそんなメソッドがあるのか・・・と思っていましたが違います!!!!!
@post.saveの後に書いてあるから、紛らわしいですね・・
save_tagの働きについては、モデルファイルで定義してます。
タグで入力した値を配列に収めてる(tag_list)を引数としてモデルファイルに渡します。
①問題のモデルファイル
ここがすごく難しい。。。
def save_tag(sent_tags)
# タグが存在していれば、タグの名前を配列として全て取得
current_tags = self.tags.pluck(:name) unless self.tags.nil?
# 現在取得したタグから送られてきたタグを除いてoldtagとする
old_tags = current_tags - sent_tags
# 送信されてきたタグから現在存在するタグを除いたタグをnewとする
new_tags = sent_tags - current_tags
# 古いタグを消す
old_tags.each do |old|
self.tags.delete Tag.find_by(name: old)
end
# 新しいタグを保存
new_tags.each do |new|
new_post_tag = Tag.find_or_create_by(name: new)
self.tags << new_post_tag
end
end
current_tags = self.tags.pluck(:name) unless self.tags.nil?
unless~は「タグが存在してるか?」を確認しています。
※投稿フォームでタグが入力されたか?を聞いてるわけではないので注意です。(ややこしいですが)
タグが存在している場合には、current_tagsに、「タグの名前を配列として」全て取得します。
「plunk」とは?
old_tags = current_tags - sent_tags
ここでは、「今あるタグ」から「新たに送られてきたタグ」を引いて、
「old_tag」に代入しています。
例えば、既に
「ネズミ」
「お花」
「コアラ」
というタグが存在していて、新たに、
「お花」
「ユニコーン」
というタグが登録されたら
old_tagには、「ネズミ」「コアラ」が入ります。
new_tags = sent_tags - current_tags
ここで、new_tagsに「ユニコーン」が入ります。
old_tags.each do |old|〜
で古いタグを消します。
今回の投稿記事付与したタグは
「お花」、「ユニコーン」です。
old_tagsに入ってる
「ネズミ」「コアラ」には用がありませんのでサヨナラします。
new_tags.each do |new|〜
で新しいユニコーンというタグを保管します。
(以下は自分なりの解釈ですので気にしないでください。)
モデルファイルを通過した時点で、手元に残ったタグは、
「お花」、「ユニコーン」です。
さらに、「ユニコーン」については「これは新しいタグ!」と判断されています。
モデルファイルは、なんだか気高い?裁判所のような?イメージです。
①タグの表示・viewとコントローラー
タグを表示していきます。(リンクは未実装です)
投稿一覧画面
def index
@posts = Post.page(params[:page]).per(10)
@tag_list=Tag.all
end
<!--タグリスト-->
<% @tag_list.each do |list|%>
<%=list.name %>
<%="(#{list.posts.count})" %><% end %>
投稿一覧画面で投稿に紐づくタグを表示
<% @posts.each do |post| %>
<i class="fas fa-tag"><%= post.tags.map(&:name).join(', ') %></i>
<%= post.tags.map(&:name).join(', ') %>
の部分、
最初、<%= post.tags.name %>にしてたのですが、エラーになってしまいました。
確かに、タグは複数ついてるので、<%= post.tags.name %>では取り出せないんだろうな。とは理解しました。
mapとは??&:とは??joinとは??
```<%= post.tags.map(&:name).join(', ') %>``` 現状、postにタグは複数ついていて、配列になってます。['お花','ゴリラ','カエル']みたいな。 さて、これを取り出して、表示させたい。 ```map(&:name)```は、配列の要素1つ1つを:nameの型にする というような意味のようです。 例を見るとわかりやすいです。 例:['a' ,'b'].map(&:upcase) #=> ["A", "B"]なので今回は、map(&:name)
の部分で["お花","ゴリラ","カエル"]と文字列にされてるのだと思います。:nameはstring型にしてるので。
そしてjoin(', ')
指定した文字で区切って連結してくれます。
"お花,ゴリラ,カエル"となってくれるわけですね
詳細画面での表示
def show
@post = Post.find(params[:id])
@post_comment=PostComment.new
@post_tags = @post.tags
end
<% @post_tags.each do |tag| %>
<%=tag.name%><%="(#{tag.posts.count})" %><% end %>
①編集機能 忘れてました..
<div class="field">
<%= f.label"タグ (,で区切ると複数タグ登録できます)" %>
<%= f.text_field :name,value: @tag_list,class:"form-control"%>
</div>
<div class="actions" style="margin:20px 0;">
<%= f.submit "投稿",class:"btn btn-outline-primary btn-block" %>
</div>
<% end %>
value: @tag_list
にしてあげないと、フォームに何も入ってない状態になってしまいます。
def edit
@post = Post.find(params[:id])
# pluckはmapと同じ意味です!!
@tag_list=@post.tags.pluck(:name).join(',')
end
def update
@post = Post.find(params[:id])
tag_list=params[:post][:name].split(',')
if @post.update(post_params)
@post.save_tag(tag_list)
redirect_to post_path(@post.id),notice:'投稿完了しました:)'
else
render:edit
end
end
注意!!このままだと、投稿編集時にタグを減らしたりするとエラーになります。
ひとまず終わり
今のままでは、投稿編集時にタグを減らすとエラーがでますので、 その対応については③の記事で説明します。
タグをリンクにして、投稿を取り出す手順については②の記事で説明します!
とても参考にさせていただきました!