Posted at

基礎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)