Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
3
Help us understand the problem. What is going on with this article?
@tseno

基礎Ruby on Rails #25 Chapter14 多対多の関連付け

More than 1 year has passed since last update.

基礎Ruby on Rails #24 Chapter13 クラウドスレレージサービス(Amazon S3)の利用
基礎Ruby on Rails Chapter15 作成中

多対多の関連付け

多対多の関連付けとは

  • 多対多の関連付けを行うには、中間テーブルを作成する。中間テーブルのカラムには2つのテーブルの主キー(ID)を定義する。

多対多の関連付けを設定するメソッド

  • CarモデルとDriverモデルを多対多で関連付けるには、中間テーブルassignmentsに対応するモデルクラスAssignmentを定義する。
class Assignment < ApplicationRecord
  belongs_to :car     # 2つのテーブルの主キーを定義する
  belongs_to :driver
end
  • Carモデルの定義は以下のようになる。
  • 「has_many ::中間テーブル名」
  • 「has_many :多対多の先のテーブル名 through: :中間テーブル名」を定義する。
  • 相手先のテーブルを中間テーブルを通じて(through)定義する。
class Car < ApplicationRecord
  has_many :assignments
  has_many :drivers, through: :assignments
end
  • Driverモデルの定義は上記と同様に以下のようになる。
class Driver < ApplicationRecord
  has_many :assignments
  has_many :cars, through: :assignments
end

多対多で関連付けられたオブジェクトの集合を操作するメソッド

  • car.driversで「その車を運転できるドライバー」の集合を取り出せる。
  • ドライバーを作成して自動車に関連付けるには、以下のように記述する。<<によってsaveメソッドを呼ばなくても自動的に保存される。
driver = Driver.new
car.drivers << driver
  • 同様に、自動車を作成してドライバーを関連付けて保存するには以下のように記述する。
car = Car.new
driver.cars << car
  • 自動車とドライバーの関連付けを外すには、destroyメソッドを使用する。
  • 以下のメソッドで、assignsテーブルから該当するレコードが削除される。(cars、driversテーブルには影響を与えない)
driver.cars.destroy(car)

「いいね」ボタンの作成(前編)

会員、記事、投票の関連付け

Voteモデルの作成

  • EntryモデルとMemberモデルの中間テーブルVoteモデルを作成する。
  • 以下を実行して、Voteモデル(votesテーブル)を作成する。
$ bin/rails g model vote
      invoke  active_record
      create    db/migrate/20181030030749_create_votes.rb
      create    app/models/vote.rb
  • entryとmemberの外部キー(entry_id、member_id)を定義する。
db/migrate/20181030030749_create_votes.rb
class CreateVotes < ActiveRecord::Migration[5.2]
  def change
    create_table :votes do |t|
      t.references :entry, null: false  # 外部キー      
      t.references :member, null: false  # 外部キー      

      t.timestamps
    end
  end
end
  • マイグレーションを実行する。
$ bin/rails db:migrate

モデル間の関連付け

  • Voteモデルには、EntryとMemberモデルに対してbelongs_toによる関連付けを行う。
app/models/vote.rb
class Vote < ApplicationRecord
  belongs_to :entry
  belongs_to :member
end
  • Entryモデルに、Memberモデルの集合を参照できるように関連付けを設定する。
  • 本来、has_many :members, through: :votesとなるところをsourceオプションを使用して、has_many :voters, through: :votes, source: :memberのように関連付けの名前をvotersに変更している。
app/models/entry.rb(一部)
class Entry < ApplicationRecord
#(省略)
  has_many :votes, dependent: :destroy
  has_many :voters, through: :votes, source: :member  # sourceオプションを使用して、membersをvotersに名前を変えている。
  • 上記の逆側の関連付けを行う。
  • 上記と同様に、has_many :entries, through: :votesとなるところをsourceオプションを使用して、has_many :voted_entries, through: :votes, source: :entryのように関連付けの名前をvoted_entriesに変更している。(entriesは他のhas_manyで使用済みのため)
app/models/entry.rb(一部)
class Member < ApplicationRecord
#(省略)
  has_many :votes, dependent: :destroy
  has_many :voted_entries, through: :votes, source: :entry

投票のルール

  • 投票のルールとして、自分自身には投票できない、1つの記事には1回しか投票できないというルールを作成する。
  • votable_for?メソッドを作成する。
app/models/member.rb(一部)
  def votable_for?(entry)
    entry && entry.author != self && !votes.exists?(entry_id: entry.id)
  end
  • Voteモデルにvotable_for?を呼び出して、ルールに合わない投票は受け付けないようにする。
  • :baseは、モデルオブジェクト全体にエラーを加えたい時に使う。
app/models/vote.rb(一部)
  validate do
    unless member && member.votable_for?(entry)
      errors.add(:base, :invalid)
    end
  end

シードデータ

  • if idx == 7 || idx == 8以下を追加する。
body =
    "今晩は久しぶりに神宮で野球観戦。内野B席の上段に着席。\n" +
#(省略)
%w(Taro Jiro Hana).each do |name|
  member = Member.find_by(name: name)
  0.upto(9) do |idx|
    entry = Entry.create(
        author: member,
        title: "野球観戦#{idx}",
        body: body,
        posted_at: 10.days.ago.advance(days: idx),
        status: %w(draft member_only public)[idx % 3])
    if idx == 7 || idx == 8                  # 以下を追加
      %w(John Mike Sophy).each do |name2|
        voter = Member.find_by(name: name2)  # entryの作成時にmemberを
        voter.voted_entries << entry
      end
    end
  end
end
  • シードデータを再投入する。
$ bin/rails db:rebuild

「いいね」ボタンの作成(後編)

ルーティング設定

  • like:「いいねボタンを押した時にvotesテーブルにレコードを作成する。
  • unlike: 自分の投票を削除する。
  • voted: 自分が投票した記事の一覧を表示する。
  • like、unlikeアクションは記事の状態を変更するので、patchにする。
  • votedアクションは集合を扱うものなので、onオプションに:collectionを指定する。
config/routes.rb(一部)
  resources :entries do
    patch "like", "unlike", on: :member
    get "voted", on: :collection
#(省略)
  end

投票数とボタンの表示

  • フッターのテンプレートに、entry.votes.countが0より大きい場合に数字を表示する。
app/views/entries/_footer.html.erb(一部)
  <% if (count = entry.votes.count) > 0 %>
  <li>
    <span class="vote"><%= count %></span>
  </li>
  <% end %>
  • 下部に、いいねボタン<%= render "votes" %>を追加する。
app/views/entries/show.html.erb(一部)
<%= render "footer", entry: @entry %>
<%= render "votes" %>
app/views/entries/_votes.html.erb
<div class="vote">
  <% @entry.voters.order("votes.created_at").each do |voter| %><%= voter.name %>
  <% end %>

  <% if current_member && current_member.votable_for?(@entry) %>
    <%= link_to "★いいね!", [:like, @entry],
      method: :patch, class: "button" %>
  <% end %>
</div>
  • 投票数が表示されることを確認した。

image.png

  • 投票の実装をする。
app/controllers/entries_controller.rb(一部)
  # 投票
  def like
    @entry = Entry.published.find(params[:id])
    current_member.voted_entries << @entry
    redirect_to @entry, notice: "投票しました"
  end
  • 投票してみる。

image.png

  • 投票されたことを確認。

image.png

自分が投票した記事一覧

unlikeアクションとvotedアクション

  • entries_controllerにunlikeアクションとvotedアクションを実装する。
  • unlikeでは、voted_entriesにdestroyメソッドをつけて、Entryオブジェクトを渡す。votesテーブルのレコードが1行削除される。
  • votedでは、throughオプション付きのhas_manyメソッドで作成したvoted_entriesに、下書き以外の記事を取り出すpublishedスコープ、order、ページネーションを追加する。
app/controllers/entries_controller.rb(一部)
  # 投票削除
  def unlike
    current_member.voted_entries.destroy(Entry.find(params[:id]))
    redirect_to :voted_entries, notice: "削除しました"
  end

  # 投票した記事一覧
  def voted
    @entries = current_member.voted_entries.published
                   .order("votes.created_at DESC")
                   .page(params[:page]).per(15)
  end

テンプレートの修正

  • 投票した一覧ページを追加する。
app/views/entries/voted.html.erb
<% @page_title = "投票した記事" %>
<h1><%= @page_title %></h1>

<% if @entries.present? %>
  <ul>
    <% @entries.each do |entry| %>
      <li>
        <%= link_to entry.title, entry %>
        by<%= link_to entry.author.name, [entry.author, :entries] %>
        <%= link_to "削除", [:unlike, entry], method: :patch, data: {confirm: "削除しますか?"} %>
      </li>
    <% end %>
  </ul>
  <%= paginate @entries %>
<% else %>
  <p>記事がありません。</p>
<% end %>
  • 投票した記事へのリンクを追加する。
app/views/entries/index.html.erb(一部)
<% @page_title = @member ? @member.name + "さんのブログ" : "会員のブログ" %>
<h1><%= @page_title %></h1>

<% if current_member %>
  <ul class="toolbar">
    <%= menu_link_to "ブログ記事の作成", :new_entry %>
    <%= menu_link_to "投票した記事", :voted_entries %>
  </ul>
<% end %>
  • 投票した記事一覧が表示された。

image.png

  • 削除できることを確認した。

image.png

まとめ

  • 多対多のテーブルを関連付けを行うときは、中間テーブルを用意する。
  • 多対多のモデルを関連付けは、has_many throuthオプション付きで指定する。
  • 中間テーブルのモデルはbelongs_toで両方のモデルを指定する。

参考
改訂4版 基礎 Ruby on Rails (IMPRESS KISO SERIES)

3
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
tseno
Java、Kotlinのフリーランスエンジニア

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
3
Help us understand the problem. What is going on with this article?