Dragon Arrow written by Tatsuya Nakaji, all rights reserved animated-dragon-image-0164

Rails ArticleモデルTagモデルで多対多 のテーブルを作る

updated on 2019-01-01

イメージ

Rails5.2 多対多


実現したいこと... articleモデルとtagモデルで多対多を実現したい


1. modelの作成

$ rails g model Article title:string body: text
$ rails g model Tag name: string

Articleのビュー、コントローラは作成しているものとする

$ rails g migration create_articles_tags

以下にmigrationファイルを成形確認

# db/migrate/xxx_create_articles.rb
class CreateArticles < ActiveRecord::Migration[5.2]
  def change
    create_table :articles do |t|
      t.string :title
      t.text :body
      t.timestamps
    end
  end
end

# db/migrate/xxx_create_tags.rb
class CreateTags < ActiveRecord::Migration
 def change
  create_table :tags do |t|
   t.string :name, null: false
   t.timestamps null: false
  end
 end
end

# db/migrate/xxx_create_articles_tags.rb
# 主キーは不要なので、:id => falseとしています。
class CreateArticlesTagsTable < ActiveRecord::Migration[5.2]
  def change
    create_table :articles_tags, :id => false do |t|
      t.integer :article_id, null: false
      t.integer :tag_id, null: false
    end
  end
end
$ rake db:migrate


2.リレーションの定義

# app/models/article.rb
class Article < ActiveRecord::Base
 has_and_belongs_to_many :tags
end
# app/models/tag.rb
class Tag < ActiveRecord::Base
 has_and_belongs_to_many :articles
end


多対多はこれで完成!!


$ rails console

> article1 = article.find(1)
> tag1 = Tag.create(name: "タグ1")
> tag2 = Tag.create(name: "タグ2")
> article1.tags << tag1 // 挿入される
> article1.tags << tag2 // 挿入される
> article1.tags.delete tag1 // article1からtag1をdelete
> article1.tags.clear // // article1から全タグをdelete


3.viewに実装

articles_controller.rb

class ArticlesController < ApplicationController
  before_action :find_article, only: [:edit, :update, :show, :destroy]


  def index
    @articles = Article.all
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)
    if @article.save
      flash[:notice] = "Successfully created article!"
      redirect_to article_path(@article)
    else
      flash[:alert] = "Error creating new article!"
      render :new
    end
  end

  def edit
  end

  def update
    if @article.update_attributes(article_params)
      flash[:notice] = "Successfully updated article!"
      redirect_to article_path(@article)
    else
      flash[:alert] = "Error updating article!"
      render :edit
    end
  end

  def show
  end

  def destroy
    if @article.destroy
      flash[:notice] = "Successfully deleted article!"
      redirect_to articles_path
    else
      flash[:alert] = "Error updating article!"
    end
  end

private

def article_params
params.require(:article).permit(:title, :body, :image, tag_ids: [])
end

def find_article
@article = Article.find(params[:id])
end

end

   tag_ids: []とした理由は tag_idsというパラメータを複数受け取ることのできるように設定するため(checkboxで複数選べる)


articles/_form.html.erb

<%= simple_form_for (@article) do |f| %>
  <% if @article.errors.any? %>
    <div id="error_explanation">
      <h2>
        <%= "#{pluralize(@article.errors.count, "error")} により保存ができませんでした" %>
      </h2>
      <ul>
        <% @article.errors.full_messages.each do |msg| %>
          <li>
            <%= msg %>
          </li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="form-group">
    <%= f.input :title, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= f.collection_check_boxes(:tag_ids, Tag.all, :id, :name) do |b| %>
      <%= b.check_box %>
      <%= b.label { b.text } %>
      </br>
    <% end %>
  </div>

<div class="form-group">
<%= f.input :image, as: :file, class: "form-control" %>
</div>

<div class="form-group">
<%= f.label :body %>
<%= f.text_area :body %>
</div>

<div class="form-group">
<%= f.button :submit, "投稿", :class => 'btn btn-primary' %>
</div>
<% end %>

ここで、collection_check_boxes において

  • 第一引数 tag_ids は, 送信するパラメータの名前
  • 第二引数 Tag.all はcheckboxのコレクションデータ
  • 第三引数 :id は, checkboxのvalue
  • 第四引数 :name はcheckboxのラベル名


以上で多対多が完璧に実装できました。    


***豆知識***

以下のやり方でも実装できますが、tagが毎回新しく増えて同じデータがたくさんできてしまいます。


articles_controller.rb

class ArticlesController < ApplicationController
  before_action :find_article, only: [:edit, :update, :show, :destroy]


  def index
    @articles = Article.all
  end

  def new
    @article = Article.new
    @article.tags.build
  end

  def create
    @article = Article.new(article_params)
    @article.tags.build(tag_params)
    if @article.save
      flash[:notice] = "Successfully created article!"
      redirect_to article_path(@article)
    else
      flash[:alert] = "Error creating new article!"
      render :new
    end
  end

  def edit
  end

  def update
    if @article.update_attributes(article_params)
      flash[:notice] = "Successfully updated article!"
      redirect_to article_path(@article)
    else
      flash[:alert] = "Error updating article!"
      render :edit
    end
  end

  def show
  end

  def destroy
    if @article.destroy
      flash[:notice] = "Successfully deleted article!"
      redirect_to articles_path
    else
      flash[:alert] = "Error updating article!"
    end
  end

private
  def tag_params
    params.require(:tag).permit(:name)
  end

  def article_params
    params.require(:article).permit(:title, :body, :image)
  end

  def find_article
    @article = Article.find(params[:id])
  end
end


articles/_form.html.erb

<%= simple_form_for (@article) do |f| %>
  <% if @article.errors.any? %>
    <div id="error_explanation">
      <h2>
        <%= "#{pluralize(@article.errors.count, "error")} により保存ができませんでした" %>
      </h2>
      <ul>
        <% @article.errors.full_messages.each do |msg| %>
          <li>
            <%= msg %>
          </li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="form-group">
    <%= f.input :title, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= fields_for :tag do |field| %>
      <%= field.label :name %>
      <%= field.text_field :name %>
    <% end %>
  </div>

  <div class="form-group">
    <%= f.input :image, as: :file, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= f.label :body %>
    <%= f.text_area :body %>
  </div>

  <div class="form-group">
    <%= f.button :submit, "投稿", :class => 'btn btn-primary' %>
  </div>
<% end %>


多対多でクエリ

articleモデルとtagモデルで多対多だが、クエリを作るときのやり方

routes.rb
...
get 'articles/:id/tag' => 'articles#tag', as: 'manage_tag' # タグのidが入る形
articles_controller.rb
...
def tag
# INNER JOINするために joinsメソッド
# 以下のようにjoinsテーブルから特定のものを引っこ抜くやり方でもクエリーを二つ作ってmergeメソッドで合体させるやり方でも良い
# @articles = Article.joins(:tags).where(tags: {id: params[:id]})
@articles = Article.joins(:tags).merge(Tag.where(id: params[:id]))
end
viewファイル(今回は_navigation.html.erb)
...
<!-- タグのリンク付きセレクトボックス, dropdownはBootstrapを使用 -->
<% Tag.all.each do |tag| %>
  <a class="dropdown-item" href=<%= manage_tag_path(id: tag.id) %>>
  <%= tag.name %>
  <div class="dropdown-divider"></div>
</a>
<% end %>
...
tag.html.erb
...
<% @articles.each do |article| %>
  <%= article.title %>
  <%= article.body %>
<% end %>
...