概要
・自身の投稿にタグ付けできるようにする機能を実装する。
・その投稿に付けられたタグで絞り込み検索ができるようになる機能を実装する。
前提
・環境
Ruby 2.6系
Rails 5.2系
・ライブラリ
Slim
・上記環境のRailsアプリ雛形
Railsアプリケーションセットアップして、deviseとSlimを導入する手順
実装
1.モデル設計とアソシエーション
・User:Post
= Userは1人ごとに多くのPostを持つので1対多
の関係。
・Post:Tag
= 1つのPostは多くのTagを持ち
、1つのTagもまた多くのPostを持つ
ことになるので多対多
の関係。
→多対多の関係ということで中間テーブルが必要になります = TagMap
→中間テーブルとは、多対多のテーブル(今回だと投稿とタグのテーブル)の外部キー(post_idとtag_id)のみを格納するテーブルのこと
で、外部キーだけで互いのテーブルを管理することを可能にする。
2.モデル作成
$ rails g devise User //deviseからUserモデルを作成 $ rails g migration add_columns_to_users name:string //nameカラム追加 $ rails g model Post content:string user:references //Postモデル作成 $ rails g model Tag tag_name:string //Tagモデル作成 $ rails g model TagMap post:references tag:references //TagMapモデル作成
・deviseで作成されたUserモデルにはカラムを追加することができないので、nameカラムを追加する場合は、新たなマイグレーションとして追加する必要がある。
・<テーブル名> : referencesで指定したテーブルに対する外部キーを張っている。(必須)
3.作成されたマイグレーションファイルの確認
3-1.usersマイグレーションファイル
db/migrate/[timestamps]_create_devise_users.rbclass DeviseCreateUsers < ActiveRecord::Migration[5.2] def change create_table :users do |t| ## Database authenticatable t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" #<省略> end end
3-2.usersテーブルへnameカラム追加
db/migrate/[timestapms]_create_add_name_to_users.rbclass AddUsernameToUsers < ActiveRecord::Migration[5.2] def change add_column :users, :name, :string end end
3-3.postsマイグレーションファイル
db/migrate/[timestamps]_create_posts.rbclass CreatePosts < ActiveRecord::Migration[5.2] def change create_table :posts do |t| t.text :content t.references :user, foreign_key: true t.timestamps end end end
3-4.tagsマイグレーションファイル
db/migrate/[timestamps]_create_tags.rbclass CreatePostTags < ActiveRecord::Migration[5.2] def change create_table :post_tags do |t| t.string :tag_name t.timestamps end end end
3-5.tag_mapsマイグレーションファイル
db/migrate/[timestamps]_create_tag_maps.rbclass CreatePostTagMaps < ActiveRecord::Migration[5.2] def change create_table :tag_maps do |t| t.references :post, foreign_key: true t.references :tag, foreign_key: true t.timestamps end end end
4.作成されたモデルファイルの確認
4-1.Userモデルファイル
app/models/user.rbclass User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_many :posts, dependent: :destroy end
・
dependent: :destroy
:親クラスに付けることで、親クラスが削除されたときに子クラスも一緒に削除することができるオプション。4-2.Postモデルファイル
app/models/user.rbclass Post < ApplicationRecord belongs_to :user has_many :tag_maps, dependent: :destroy has_many :tags, through: :tag_maps end
・
throughオプションによって、tag_mapsテーブルを通してtagsテーブルとの関連付けを行っています。
こうすることで、Post.tags
とすればPostに紐付けられたTagの取得が可能になります。これは投稿詳細画面などでその投稿に付けられているタグを取得して表示させる
などの際に使えます。・throughオプションを使う場合、先にその中間テーブルとの関連付けを行う必要があります。
・中間テーブルにdependent: :destroyオプションを付けることで、Postが削除されると同時にPostとTagの関係が削除されます。
4-4.Tagモデルファイル
app/models/tag.rbclass Tag < ApplicationRecord has_many :tag_maps, dependent: :destroy, foreign_key: 'tag_id' has_many :posts, through: :tag_maps end
・tag_mapsテーブルとの関連付けを行ってから、tag_mapsを通してpostsテーブルと関連づけています
。Tag.posts
とすれば、タグに紐付けられたPostを取得できる。これは特定のタグ、例えば「スポーツ」などといったタグを持った投稿を検索したい時
などに使えます。4-5.TagMapモデルファイル
app/models/tag_map.rubclass PostTagMap < ApplicationRecord belongs_to :post belongs_to :tag validates :post_id, presence: true validates :tag_id, presence: true end
・複数のPost、複数のTagに所有されるのでbelongs_toで関連付け。
・PostとTagの関係を構築する際、2つの外部キーが存在することは絶対なので、バリデーションを張ります。
5.ルーティング設定
config/routes.rbRails.application.routes.draw do devise_for :users root to: 'posts#index' resources :posts end
・この時点ではひとまず、deviseで用意されているルーティングと、ルートパスと、postsリソースへのパスが設定されるようにします。
まずは、投稿に対してタグ付けできる機能から実装
6.コントローラを作成
6-1.postsコントローラのcreateアクションに、投稿とタグを作成するコードを書いてく。
app/controllers/posts_controller.rbclass PostsController < ApplicationController def create @post = current_user.posts.new(post_params) tag_list = params[:post][:tag_name].split(nil) if @post.save @post.save_tag(tag_list) redirect_back(fallback_location: root_path) else redirect_back(fallback_location: root_path) end end private def post_params params.require(:post).permit(:content) end end
上記コードを分解して見てみます。
ログイン中のユーザーの投稿作成@post = current_user.posts.new(post_params)
・
current_user.posts
とすることで、ログイン中のユーザーのidがpostsテーブルのuser_idに保存されるようになりますが、UserとPostの関連付けを行っていない場合はエラーになります
。引数にはStrong Parametersを使ってる。送信されてきたタグの取得tag_list = params[:post][:tag_name].split(nil)
・
params[:post][:tag_name]
:formから、@postオブジェクトを参照してタグの名前も一緒に送信するのでこの形で取得する。例えば「"スポーツ" "勉強" "お仕事"」みたいな感じで送られてくる。・
.split(nil)
:送信されてきた値を、スペースで区切って配列化する。上記の例でいうと["スポーツ" "勉強" "お仕事"]みたいな感じ。配列化するのは、あとでこの値をデータベースに保存する際に一つずつ繰り返し処理で取り出す必要があるため。タグの保存@post.save_tag(tag_list)
・
先ほど取得したタグの配列をデータベースに保存する処理
。その処理を行うsave_tagの定義は後述する。6-2.indexアクションに投稿とタグを取得するコードを書いてく。
app/controllers/posts_controller.rbdef index @tag_list = Tag.all #ビューでタグ一覧を表示するために全取得。 @posts = Post.all #ビューで投稿一覧を表示するために全取得。 @post = current_user.posts.new #ビューのform_withのmodelに使う。 end
app/controllers/posts_controller.rbdef show @post = Post.find(params[:id]) #クリックした投稿を取得。 @post_tags = @post.tags #そのクリックした投稿に紐付けられているタグの取得。 end
7.Postモデルファイルに、save_tagインスタンスメソッドを定義する。
・先ほどcreateアクションで記述したsave_tagインスタンスメソッドの中身を定義していきます。
app/models/post.rbclass Post < ApplicationRecord #<省略> def save_tag(sent_tags) current_tags = self.tags.pluck(:tag_name) unless self.tags.nil? old_tags = current_tags - sent_tags new_tags = sent_tags - current_tags old_tags.each do |old| self.post_tags.delete PostTag.find_by(tag_name: old) end new_tags.each do |new| new_post_tag = PostTag.find_or_create_by(tag_name: new) self.post_tags << new_post_tag end end end
現在存在するタグの取得current_tags = self.tags.pluck(:tag_name) unless self.tags.nil?
・先ほどcreateアクションにて保存した@postに紐付いているタグが存在する場合、「タグの名前を配列として」全て取得します。
古いタグの取得old_tags = current_tags - sent_tags
・現在取得した@postに存在するタグから、送信されてきたタグを除いたタグをold_tagsとします。
新しいタグの取得new_tags = sent_tags - current_tags
・送信されてきたタグから、現在存在するタグを除いたタグをnew_tagsとする。
古いタグの削除old_tags.each do |old| self.tags.delete Tag.find_by(tag_name: old) end
・古いタグを削除します。
新しいタグの保存new_tags.each do |new| new_post_tag = Tag.find_or_create_by(tag_name: new) self.post_tags << new_post_tag end
・新しいタグをデータベースに保存する。
・ただタグを一つずつ取得して保存するだけでなく、なぜ上記のようにいちいち古いタグと新しいタグを取得して削除する手間をとるかというと、例えば投稿を編集する場合にそういった動作が必要になるからです。
しかしながら、すみません今回は編集機能の実装は省略します。
8.ビューを作成する。
8-1.投稿一覧のビューを作成する。
app/views/posts/index.html.slimh3 タグリスト - @tag_list.each do |list| span = link_to list.tag_name, tag_posts_path(tag_id: list.id) = "(#{list.posts.count})" hr h3 投稿する = form_with(model: @post, url: posts_path, local: true) do |f| = f.text_area :content br = "スペースを入力することで複数のタグを付けることができます。" = "例:音楽 文芸 スポーツ" = f.text_field :tag_name br = f.submit hr h3 投稿一覧 - @posts.each do |post| = link_to post.content, post
上記のコードを分解して見てみます。
タグ一覧の表示- @tag_list.each do |list| span = link_to list.tag_name, tag_posts_path(tag_id: list.id) = "(#{list.posts.count})"
・indexアクションで全取得したタグを全て表示して、尚且つ
そのタグに関連する投稿を表示するパスへのリンクを張っています
。このリンクをクリックすることで、そのタグに関連付けられた投稿を表示する
ようにします。このリンクを取得するルーティングの設定と、アクションの作成は後述。・
"(#{list.posts.count})"
では、現在そのタグを持つ投稿が幾つ存在するかをカウントして表示しています。新規投稿フォーム(タグ付け可能)= form_with(model: @post, url: posts_path, local: true) do |f| = f.text_area :content br = "スペースを入力することで複数のタグを付けることができます。" = "例:音楽 文芸 スポーツ" = f.text_field :tag_name br = f.submit
・
= f.text_field :tag_name
:この行を記述することで、フォームに入力されたタグが、params[:post][:tag_name]というパラメータとしてcreateアクションへ送信されることになる。8-2.投稿詳細ページのビューを作成する。
app/views/posts/show.html.slimh1 投稿詳細 p= @post.content br = "タグ: " - @post_tags.each do |tag| span = link_to tag.tag_name, tag_posts_path(tag_id: tag.id) = "(#{tag.posts.count})" hr = link_to 'ホーム', root_path
・タグを表示する部分は、一覧表示で記述した方法と同じです。この場合だと
特定の投稿に対して紐付けられたタグ
という部分が異なります。
・ここまでで、投稿に対してタグ付けすることができる機能の実装が完了です。
タグで投稿を絞り込み表示する機能を実装する
1.ルーティングを追加する。
config/routes.rbRails.application.routes.draw do devise_for :users root to: 'posts#index' resources :posts #タグによって絞り込んだ投稿を表示するアクションへのルーティング resources :tags do get 'posts', to: 'posts#search' end end
・ネストすることで、先ほど記述したような
tag_posts_path(tag_id: tag.id)
という、特定のタグに紐付けられた投稿ページへ遷移させるためのパスが使えるようになる。
2.postsコントローラにsearchアクションを作成する。
app/controllers/posts_controller.rbdef search @tag_list = Tag.all #こっちの投稿一覧表示ページでも全てのタグを表示するために、タグを全取得 @tag = Tag.find(params[:tag_id]) #クリックしたタグを取得 @posts = @tag.posts.all #クリックしたタグに紐付けられた投稿を全て表示 end
3.タグで絞り込んだ投稿一覧を表示するページのビューを作成する。
・先に、app/views/postsディレクトリ内に、search.html.slimファイルを作成する。
app/views/posts/search.html.slimh2 投稿一覧 #タグリスト - @tag_list.each do |list| span = link_to list.tag_name, tag_posts_path(post_tag_id: list.id) = "(#{list.posts.count})" br #タグによって絞り込んだ投稿一覧 = "タグが ─ " strong= "#{@tag.tag_name}" = " ─ の投稿一覧" br - @posts.each do |post| = link_to post.content, post br = link_to 'ホーム', root_path
・タグリストの部分はindexの部分と同じです。
= "タグが ─ " strong= "#{@tag.tag_name}" = " ─ の投稿一覧"
・この部分では、どんなタグによって絞り込みされたのか分かるように、当のタグを表示させています。
・以上で、タグで絞り込んだ投稿を表示する機能の実装が完了です。見れば分かる通り、ビューでは重複する箇所がいくつか存在するので、その部分はパーシャル化してリファクタリングするとビューがすっきりして見栄えがよくなるかと思います。