
updated on 2019-01-01
実現したいこと... 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
endtag_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が毎回新しく増えて同じデータがたくさんできてしまいます。
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
endarticles/_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 %> ...