前 基礎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>
- 投票数が表示されることを確認した。
- 投票の実装をする。
app/controllers/entries_controller.rb(一部)
# 投票
def like
@entry = Entry.published.find(params[:id])
current_member.voted_entries << @entry
redirect_to @entry, notice: "投票しました"
end
- 投票してみる。
- 投票されたことを確認。
自分が投票した記事一覧
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 %>
- 投票した記事一覧が表示された。
- 削除できることを確認した。
まとめ
- 多対多のテーブルを関連付けを行うときは、中間テーブルを用意する。
- 多対多のモデルを関連付けは、has_many throuthオプション付きで指定する。
- 中間テーブルのモデルはbelongs_toで両方のモデルを指定する。