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 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が毎回新しく増えて同じデータがたくさんできてしまいます。
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 %> ...