「Rails」gem無し 多対多のタグ機能を実装した【toxi法】

2019/10/01◉ 14 views


Toxi法でタグ機能を実装する


今回gem無しでタグ機能を実装してみました。
本来はgemでサクッとできるものなんですが、勉強にもなる為gemは使わずに多対多のtoxi法というやり方で実装していきました。

もちろん自分だけでは何もわからないので、今回もググって参考記事見てやってみました。

参考記事はこちら
railsでタグ機能を実装する

まずはどんな感じにできたか見てください。

  • タグ入力フォーム

入力

  • ブログ記事のビュー

記事



  • タグリスト
タグリスト

見た目はこんな感じに表示させています。
めっちゃハマったし、1人で完成までやれなかったけどやり方を説明していこうと思います。

多対多の関連付けをする


まず多対多ってどーゆーことかと言うと、1つの記事に複数のタグ付けができて、タグも複数の記事にタグ付けされる状態のことです。

1つの記事に2つのタグを付けれるとします。でもそのタグは他の記事にもタグ付けされています。

複数の記事と複数のタグ、これらを関連付けして行くのが中間テーブルになります。

僕のブログサイトで例えるとblogsテーブル(記事)があり、tagsテーブル(タグ)があり、その中間にtagmapsテーブル(記事とタグ)があります。

そのためにはまず、modelを作成します。

modelの作り方は、省略します。

modelを作ってできたマイグレーションファイルがこちらになります。

blogsテーブルには記事情報が入ります
class CreateBlogs < ActiveRecord::Migration[6.0]
  def change
    create_table :blogs do |t|
      t.string :title
      t.text :body
      t.integer :status, default: 0, null: false
      t.datetime :time
      t.timestamps
    end
  end
end

tagsテーブルにはタグが入ります
class CreateTags < ActiveRecord::Migration[6.0]
  def change
    create_table :tags do |t|
      t.string :tag_name

      t.timestamps
    end
    add_index :tags, :tag_name, unique: true
  end
end

tagmapsテーブルには記事のidとタグのidが入ります
class ChangeColumunsTagmaps < ActiveRecord::Migration[6.0]
  def change
    create_table :tagmaps do |t|
      t.integer :blog_id
      t.integer :tag_id

      t.timestamps
    end
  end
end

tagsテーブルのtag_nameには、add_indexでユニーク(一意制)を設けています。

db:migrate後のschema.rbはこちら

create_table "blogs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "title"
    t.text "body"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.integer "status", default: 0, null: false
    t.datetime "time"
  end

  create_table "tagmaps", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.integer "blog_id"
    t.integer "tag_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "tags", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "tag_name"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["tag_name"], name: "index_tags_on_tag_name", unique: true
  end

ここからモデル間で関連付けをさせていきます。

関連付けにはhas_manyを使うことで、設定できるようにします。

blog.rb
has_many :tagmaps, dependent: :destroy
has_many :tags, through: :tagmaps
ここはhas_manyを使ってblogtagmapsと関連付けさせて、尚且つdependent: :destroyを追加して、削除する時に全て消せるようにしています。

記事を消したら、tagmapsのデータも消してくれたりできますね。

その次にhas_manyを使って同じくblogtagsと関連付けさせます。

そしてthrough: :tagmapsを設定することで、throughオプションによりtagmaps経由でtagsにアクセスできるようになります。
tag.rb
class Tag < ApplicationRecord
  has_many :tagmaps, dependent: :destroy
  has_many :blogs, through: :tagmaps
end
tagtagmapsと関連付けさせています。

そしてtagmaps経由でblogsにアクセスできるようになってます。
tagmap.rb
class Tagmap < ApplicationRecord
  belongs_to :blog
  belongs_to :tag
end
最後にtagmapbelongs_toを使って、blogtagに関連付けます。

これらの関連をすることで、blogからtagsが取れるし、tagからblogsが取れるようになります。

取り方は後ほど説明していこうと思います。

タグ情報をDBに新規で登録できるようにする


記事投稿する時にタグを登録するような機能にします。

新しいタグならtagに保存できるようにし、既存のタグなら重複を避けます。

まずは完成してるコードはこちら

blogs.controller.rb
  def create
    @blog = Blog.new(blog_params)
    tag_list = params[:blog][:tag_name].split(",")
    respond_to do |format|
      if @blog.save
        @blog.save_blogs(tag_list)
        format.html { redirect_to @blog, notice: '記事を投稿しました' }
        format.json { render :show, status: :created, location: @blog }
      else
        format.html { render :new }
        format.json { render json: @blog.errors, status: :unprocessable_entity }
      end
    end
  end

今回タグ機能で追加したところはこれ
tag_list = params[:blog][:tag_name].split(",")
フォームから送られてきたタグネームはsplit(",")で区切りを付けて、blogとは別にtagに保存するようにします。
これでRails,タグ機能とかで2つのタグを配列で送れるようになります。
params[:tag_name]だとエラーになります。
undefined method `split' for nil:NilClass
@blog.save_blogs(tag_list)
save_blogsメソッドを作って、タグを保存していきます。

save_blogsメソッドはモデル側で設定していきます。

こちらが完成してるコード

blog.rb
  def save_blogs(tags)
    current_tags = self.tags.pluck(:tag_name) unless self.tags.nil?
    old_tags = current_tags - tags
    new_tags = tags - current_tags
  
    # Destroy
    old_tags.each do |old_name|
      self.tags.delete Tag.find_by(tag_name:old_name)
    end

    # Create
    new_tags.each do |new_name|
      blog_tag = Tag.find_or_create_by(tag_name:new_name)
      self.tags << blog_tag
    end
  end

頑張って説明してみます。

self.tags.pluck(:tag_name)では、tagsテーブルのtag_nameカラムの一覧を取り出します。

unless self.tags.nil?では
unless文 → もしも評価が偽(false)ならば〜〜する
nil?→ nilの場合のみtrueを返し、それ以外はfalseを返す


old_tags = current_tags - tags
既存のタグの配列から配列を除外しています。

例で例えると、既存のタグの配列
current_tags=["Actiontext", "adsense", "Rails"]だとします。

tagsにはtags=["adsense", "tast"]だとします。

除外するとold_tags = ["Actiontext"]となります。


new_tags = tags - current_tags
こちらも配列から配列を除外

例で例えると、tagsの配列
tags=["adsense", "test"]だとします。

current_tags=["Actiontext", "adsense", "Rails"]だとします。

除外するとnew_tags = ["test"]となります


# Destroyold_tags = ["Actiontext"]だとします。
self.tagsは、クラスメソッドの意味で、クラスとはblogのことです。
なのでblog.tagsの配列から、Tag.find_byで検索して取得した["Actiontext"]を削除します。
その時にtagmapsから削除してくれますね。


# Createnew_tags = ["test"]だとします。
find_or_create_by
は、オブジェクトが存在する場合は取得、なければ作成します。
なので今回の例でいうと、testが作成されます。
blog_tag = #<Tag id: 14, tag_name: "test", created_at: "2019-09-29 15:01:27", updated_at: "2019-09-29 15:01:27">

self.tags << blog_tagは、self.tags.push(blog_tag)と一緒の意味になり、push()とは配列の要素を追加する場合に使います。

これらのことを使ってcontrollercreateアクション@blog.save_blogs(tag_list)で、save_postメソッドを実行し、新規のtagは保存しているらしいです。

正直参考記事見てやってるのでうまく説明できないですw

ただ頭ではなんとなくわかってるけど、説明ができない...すいません。

今もググりながら記事書いてますw

とまーこんな感じでタグの保存をしているわけですね。

次にupdateアクションでも同じように設定します。
  def update
    tag_list = params[:blog][:tag_name].split(",")
    respond_to do |format|
      if @blog.update(blog_params)
        @blog.save_blogs(tag_list)
        format.html { redirect_to @blog, notice: '記事を編集しました' }
        format.json { render :show, status: :ok, location: @blog }
      else
        format.html { render :edit }
        format.json { render json: @blog.errors, status: :unprocessable_entity }
      end
    end
  end

editアクションでは編集画面で入力フォームにタグを表示させる為に必要でした。
  def edit
    @tag_list = @blog.tags.pluck(:tag_name).join(",")
  end
.pluckメソッドは、指定したカラムの配列を取得することができます。
関連付けしているので、@blog.tagsでタグを取得できるようになってます。

最後にフォームから入力して送信できるようにします。
form.html.erb
<div class="tag">
    <%= form.label :tag %>
    <%= form.text_field :tag_name, value: @tag_list %>
</div>
value: @tag_listで編集時はここにタグ付けしたタグが、入力されています。


これでタグ機能は完成ですが、今度はタグをviewで表示させてみます。

登録したタグの表示


ここでハマってしまい2日悩んでも自力じゃやれませんでした....

僕の入っているコミュニティーで相談したところ、強強な、にゅ〜ぶるさんが教えてくれてなんとかできるようになったので、おさらいがてらまとめようと思う。

まず完成したコードはこちら

ブログ記事一覧にタグ付を表示する
  def index
    if params[:tag_id]
      @tag = Tag.find(params[:tag_id])
      @blogs = @tag.blogs.published.order(time: "DESC").page(params[:page]).per(10)
      @blogs_side = Blog.published.order(time: "DESC")
    else
      @blogs = Blog.published.order(time: "DESC").page(params[:page]).per(10)
      @blogs_side = Blog.published.order(time: "DESC")
    end
    respond_to do |format|
      format.html
      format.rss { render :layout => false }
    end
  end
関係してるとこだけ説明します。

  • まずは全記事にタグ付け表示
ここでは特にコントローラー側ではすることないです。

@blogs = Blog.publishedで公開記事を@blogsに入れておきます。

次にビューにてeachで1つずつ記事を取り出します。

<% @blogs.each do |b| %>

これで記事タイトルやbody等が表示させれます。

次に記事のタグを表示してみます。

<% b.tags.each do |t| %>

これで1つの記事からタグを取得できるようになります。

blog.tagsってことですね!

そしたら次は、そのタグの名前でリンク付で表示させます。

<%= link_to t.tag_name, blogs_path(tag_id: t.id) %>
                    
t.tag_nameで、そのタグの名前を表示させます。

そしてリンクには、t.idでタグのidtag_idで送ります。

これでタグをクリックすることで、タグの絞り込み表示をさせていきます。

  • タグの絞り込みで関連記事を表示
コントローラー側でifで条件を設定します。

if params[:tag_id] もしtag_idが送られてきたらって意味ですね。

送られてきたtag_id
@tag = Tag.find(params[:tag_id])

Tagから検索して取り出します。

そして取り出したタグと関係する記事一覧を取得します。
@blogs = @tag.blogs.published

@tag.blogsは関連付けさせてるから、タグから記事一覧が取得できますね!

これで記事一覧画面のタグを表示させて、そのタグをクリックしたら、そのタグで絞り込んだ記事一覧が表示されます。

これだけで完了なのに2日かかった僕は....情けないですw

助けていただいたにゅ〜ぶるさんありがとうございました!!!!

以下にゅ〜ぶるさんのありがたいお言葉

起点がTagからだから、Tag.blogsだよー
神様


本当に感謝してますにゅ〜ぶるさん!

具現化してサイドバーにタグリストを作る


次はビューにて、タグを全部表示させてタグリストを作ります。

最終的なコードはこちら

controller
  def index
    if params[:tag_id]
      @tag_list = Tag.all #追加
      @tag = Tag.find(params[:tag_id])
      @blogs = @tag.blogs.published.order(time: "DESC").page(params[:page]).per(10)
      @blogs_side = Blog.published.order(time: "DESC")
    else
      @tag_list = Tag.all #追加
      @blogs = Blog.published.order(time: "DESC").page(params[:page]).per(10)
      @blogs_side = Blog.published.order(time: "DESC")
    end
    respond_to do |format|
      format.html
      format.rss { render :layout => false }
    end
view
  end<p>タグリスト</p>
  <% @tag_list.each do |list| %>
     <p><%=link_to list.tag_name,blogs_path(tag_id:list.id)%><%= list.blogs.published.count %></p>
  <% end %>


ここも僕はまだ理解ができてなくて、にゅ〜ぶるさんがヒントをくれました。

本当に感謝しかありませんね。

以下ありがたいお言葉

具現化



それでもできなかった僕は、もう一度相談して再度ヒントをもらいました。

情けないです...

ヒント


やりたいことをまず日本語で具現化する。これがとても大事なことだと教えてもらいました。

初心者の方はぜひやってみてください。

そして苦戦しながらもやっていきました。

  • 全てのタグを順番にループして

まずコントローラー側で全てのタグを取得する
@tag_list = Tag.all
それをループさせる
<% @tag_list.each do |list| %>

  • タグ名を表示して

これはさっきやったからわかりますね!
<%=link_to list.tag_name,blogs_path(tag_id:list.id)%>

  • その横に

<%=link_to list.tag_name,blogs_path(tag_id:list.id)%> ここに!

  • そのタグが使われている記事のカウントも一緒に表示する

ここで僕は勘違いしてました。
<%=link_to list.tag_name,blogs_path(tag_id:list.id)%><%= list.tag_name.blogs.count %>

これだとlist.tag_nameは、「そのタグ」ではなく「そのタグのタグ名」でした。

そのタグとはlistのことで、list.blogsで、そのタグが使われてる記事ってことになります。

それで、カウントも一緒に表示
<%=link_to list.tag_name,blogs_path(tag_id:list.id)%><%= list.blogs.published.count %>

ちなみにpublishedenumで設定した公開記事って意味です。

これで無事に全タグを表示させて、そのタグと関係してるタグがいくつあるかカウントさせることができました!

あとはこれをデザインして表示させたら完成です!

今回は1人で2日かかってもできずに心が折れかけました。

独学でやってても限界があると最近すごく思います。

友人に相談できる人がいたら、気軽に聞けるけど、そんなプログラミングしてる友人はいないし、むしろ友人少ないwww

スクール行くお金と時間もないし、メンターも僕には安くないし、ちょっと恐いw

そんな中コミュニティーを募集してる方を見つけて、思い切ってコミュニティーに参加させてもらったのが今のにゅ〜ぶる会でした。

色んなユニークな方々がいっぱいいて、毎日楽しい会話と日々の分報が見れて、いつも僕も頑張ろうという気持ちにさせてくれます。

わからないことは質問スレで質問できるし、自分のスレで悩みを呟いてたら、誰かが助けてくれたりヒントをくれたり、応援したり励ましてくれたり、笑わしてくれたりとてもいい環境です!

独学でやっていてちょっときついなーって思ってる方がいたら、そーゆーコミュニティーがあれば思い切って入ってみることをお勧めします。

以上 タグ機能+ちょっと宣伝でしたw


このブログサイトは初学者の僕が独学で
Ruby on Railsで作ったブログサイトです。 間違っている所もあるかもしれません。 あくまで参考程度にしていただけたらなと思います。 何かありましたらお問い合わせか、Twitter@ちょめこよりご連絡下さい。