前 基礎Ruby on Rails Chapter9 ActiveRecord/スコープ/ページネーション
次 基礎Ruby on Rails Chapter10 モデル間の関連付け/ブログ機能の追加(コントローラ/ビュー編)
関連付けの概要
モデル間の関連付けと外部キー
-
テーブルの中で、別のテーブルの主キーを参照するカラムのことを外部キーという。
-
変数@carにモデルクラスCarのインスタンスがセットされているとき、次の式はその車輪(Wheel)の集合を表すリレーションオブジェクトを返す。
Wheel.where(car_id: @car.id)
- しかし、Rubyではもっと直感的な書き方がある。
@car.wheels
- wheelsはCarクラスのインスタンスメソッド。
- where、order、firstなどのメソッドを連結することができる。
@car.wheels.where(coloer: "red").order(:created_at).first
- wheelメソッドを使うには、以下のように
has_many
で子を定義する必要がある。
has_many :wheels
関連付けを作るメソッド
1対多の関連付け
- 親は、子を
has_many
で定義する。
class Car < ApplicationRecord
has_many :wheels
end
- 子は、親を
belongs_to
で定義する。
class Wheel < ApplicationRecord
belongs_to :car
end
-
これにより、
@car.wheels
で参照元のモデルオブジェクトの集合を取り出したり、@wheel.car
で参照先のモデルオブジェクトを取り出したりできる。 -
車輪を作成し、自動車に関連付けて保存する方法
@wheel = Wheel.new
@wheel.car = @car
@wheel.save
- 逆に、自動車のほうから車輪を関連付けるには、
<<
で追加する。 - これにより、関連付けと車輪のレコードの保存が同時に行われる。
@car.wheels << @wheel
- 車輪を自動車に結び付け、保存は行わないようにするには、wheels.buildのようにbuildメソッドを使う。引数にはハッシュでモデルの属性を指定できる。
@car.wheels.build(name: "車輪1")
# ハッシュを複数指定することができる
@car.wheels.build({ name: "車輪1" },{ name: "車輪2" })
- wheelsメソッドが返すのはリレーションオブジェクト。なので、集計用のメソッドやクエリーメソッドを呼び出せる。
# 車輪の数を返す
@car.wheels.count
# クエリーメソッドを返す
@car.wheels.order("created_at DESC")
命名規約とオプション
has_manyとbelongs_toでモデル間の関連付けを表すときには、以下のようなルールがある。
-
外部キーのカラム名は、
参照先のテーブル名の単数形_id
。例、car_id
-
belongs_toに指定する名前は、テーブル名の単数形
-
has_manyに指定する名前は、テーブル名の複数形
-
外部キーのカラム名がルールと異なるときは、
foreign_key
オプションでカラム名を指定する。
# car.idとengine.vihicle_idでjoinする(cat_idではなく)
class Car < ApplicationRecord
has_many :engines, foreign_key: "vihicle_id"
end
class Wheel < ApplicationRecord
belongs_to :car, foreign_key: "vihicle_id"
end
- 関連付けで使われるメソッド名を変えたい場合は、class_nameオプションを使う。
- メソッド名をmotorsでなく、enginesにしたい場合は、class_nameオプションに本当のクラス名を指定する。
# car.enginesが使えるようになる。実際はMotorモデルが存在する。
class Car < ApplicationRecord
has_many :engines, class_name: "Motor"
end
- dependentオプションを:destroyにすると、参照先のレコードを削除した時に、参照元のレコードも自動的に削除される。
- dependentオプションを:nullifyにすると、参照先のレコードを削除した時に、参照元の外部キーがNULLになる。
class Car < ApplicationRecord
has_many :wheel, dependent: :destroy
end
会員ブログ関連モデルの準備
ブログ記事の関連付け
Entryモデルの作成
-
rails g
コマンドで、Entryモデルを作成する。
$ bin/rails g model entry
invoke active_record
create db/migrate/20181006230957_create_entries.rb
create app/models/entry.rb
- 上記で生成されたマイグレーションスクリプトを修正する。
-
t.references :member
は外部キーの定義で、entriesテーブルに整数型のmember_idカラムが追加される。- referencesを使うと、外部キーにインデックスが設定される。設定したくない場合は、
, index: false
を追加する。
- referencesを使うと、外部キーにインデックスが設定される。設定したくない場合は、
db/migrate/20181006230957_create_entries.rb
class CreateEntries < ActiveRecord::Migration[5.2]
def change
create_table :entries do |t|
t.references :member, null: false # 外部キー
t.string :title, null: false # タイトル
t.text :body # 本文
t.datetime :posted_at, null: false # 投稿日
t.string :status, null: false, default: "draft" # 状態(draft/member_only/public)
t.timestamps
end
end
end
- マイグレーションを実行する。
- マイグレーションスクリプトに誤りがあってエラーが出た場合は、修正後
bin/rails db:rebuild
を実行する。
$ bin/rails db:migrate
モデル間の関連付け
- Memberモデルにhas_manyメソッドでEntriesと関連付ける。member.id=entry.member_id
- 会員はブログ記事を複数持つ。メンバーは削除されると、記事も削除される。
app/models/member.rb(一部)
has_many :entries, dependent: :destroy
- 子モデルは、親モデルを定義する。authorメソッドを追加し、本当のモデルであるMemberを指定する。
- 外部キーは、member_idとする。
app/models/entry.rb
class Entry < ApplicationRecord
belongs_to :author, class_name: "Member", foreign_key: "member_id"
end
Entryモデルでの準備
モデルの属性名の日本語化
- 以下のようにモデルの属性名をロケールテキストに追加する。
config/locales/ja.yml(一部)
ja:
activerecord:
models:
#(省略)
entry: ブログ記事
#(省略)
attributes:
entry:
title: タイトル
body: 本文
posted_at: 日時
status: 状態
status_draft: 下書き
status_member_only: 会員限定
status_public: 公開
バリデーション
- titleは空禁止、200文字以内。
- 本文と投稿日は空禁止。
- 状態は、下書き/会員限定/公開のいずれか。
app/models/entry.rb(一部)
STATUS_VALUES = %w(draft member_only public)
validates :title, presence: true, length: {maximum: 200}
validates :body, :posted_at, presence: true
validates :status, inclusion: { in: STATUS_VALUES }
ブログ記事を絞り込むスコープ
- 以下のようにスコープを追加する
app/models/entry.rb(一部)
# 公開記事のみ
scope :common, -> {where(status: "public")}
# 下書き以外(公開・会員限定)
scope :published, -> {where("status <> ?", "draft")}
# 下書き以外と、自分が書いた記事全部
scope :full, -> (member) {where("status <> ? OR member_id = ?", "draft", member.id)}
# ログインしていればfull、していなければcommon
scope :readable_for, -> (member) {member ? full(member) : common}
ビュー用のメソッド
app/models/entry.rb(一部)
class << self
# statusを引数に、日本語文字列を取得する
def status_text(status)
I18n.t("activerecord.attributes.entry.status_#{status}")
end
# [["下書き", "draft"], ...]のような配列を作成する。画面上のリストに使用する。
def status_options
STATUS_VALUES.map {|status| [status_text(status), status]}
end
end
シードデータ
- シードデータの読み込み設定にテーブルを追加する。
entries
テーブルを読み込みする。
db/seeds.rb(一部)
table_names = %w(members articles entries)
#(省略)
- 以下のようなシードデータを追加する。
db/seeds/development/entries.rb
body =
"今晩は久しぶりに神宮で野球観戦。内野B席の上段に着席。\n" +
"先発はヤクルトがブキャナン、広島はジョンソン。" +
"2回裏に中村選手のセーフティスクイズなどでヤクルトが3点を先取。" +
"そして、8回裏には代打・荒木選手がレフトスタンドへ2号満塁ホームラン。\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])
end
end
- データベースの再構築を行い、シードデータをセットする。
bin/rails db:rebuild
- データが無事入りました。