基礎Ruby on Rails Chapter10 モデル間の関連付け／ブログ機能の追加（モデル編）

前 [基礎Ruby on Rails Chapter9 ActiveRecord／スコープ／ページネーション](https://qiita.com/tseno/items/894e107e58a58e4ff8db)
次 [基礎Ruby on Rails Chapter10 モデル間の関連付け／ブログ機能の追加（コントローラ／ビュー編）](https://qiita.com/tseno/items/4c8fca375a8e20fa34a1)

# 関連付けの概要

## モデル間の関連付けと外部キー

- テーブルの中で、別のテーブルの主キーを参照するカラムのことを外部キーという。

- 変数@carにモデルクラスCarのインスタンスがセットされているとき、次の式はその車輪(Wheel)の集合を表すリレーションオブジェクトを返す。

```rb
Wheel.where(car_id: @car.id)
```

- しかし、Rubyではもっと直感的な書き方がある。

```rb
@car.wheels
```

- wheelsはCarクラスのインスタンスメソッド。
- where、order、firstなどのメソッドを連結することができる。

```rb
@car.wheels.where(coloer: "red").order(:created_at).first
```

- wheelメソッドを使うには、以下のように`has_many`で子を定義する必要がある。

```rb
has_many :wheels
```

## 関連付けを作るメソッド

### 1対多の関連付け

- 親は、子を`has_many`で定義する。

```rb
class Car < ApplicationRecord
  has_many :wheels
end
```

- 子は、親を`belongs_to`で定義する。

```rb
class Wheel < ApplicationRecord
  belongs_to :car
end
```

- これにより、`@car.wheels`で参照元のモデルオブジェクトの集合を取り出したり、`@wheel.car`で参照先のモデルオブジェクトを取り出したりできる。

- 車輪を作成し、自動車に関連付けて保存する方法

```rb
@wheel = Wheel.new
@wheel.car = @car
@wheel.save
```

- 逆に、自動車のほうから車輪を関連付けるには、`<<`で追加する。
- これにより、関連付けと車輪のレコードの保存が同時に行われる。

```rb
@car.wheels << @wheel
```

- 車輪を自動車に結び付け、保存は行わないようにするには、wheels.buildのようにbuildメソッドを使う。引数にはハッシュでモデルの属性を指定できる。

```rb
@car.wheels.build(name: "車輪1")
# ハッシュを複数指定することができる
@car.wheels.build({ name: "車輪1" },{ name: "車輪2" })
```

- wheelsメソッドが返すのはリレーションオブジェクト。なので、集計用のメソッドやクエリーメソッドを呼び出せる。

```rb
# 車輪の数を返す
@car.wheels.count
# クエリーメソッドを返す
@car.wheels.order("created_at DESC")
```

### 命名規約とオプション

has_manyとbelongs_toでモデル間の関連付けを表すときには、以下のようなルールがある。
- 外部キーのカラム名は、`参照先のテーブル名の単数形_id`。例、`car_id`
- belongs_toに指定する名前は、テーブル名の単数形
- has_manyに指定する名前は、テーブル名の複数形

- 外部キーのカラム名がルールと異なるときは、`foreign_key`オプションでカラム名を指定する。

```rb
# 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オプションに**本当のクラス名**を指定する。

```rb
# car.enginesが使えるようになる。実際はMotorモデルが存在する。
class Car < ApplicationRecord
  has_many :engines, class_name: "Motor"
end
```

- dependentオプションを:destroyにすると、参照先のレコードを削除した時に、参照元のレコードも自動的に削除される。
- dependentオプションを:nullifyにすると、参照先のレコードを削除した時に、参照元の外部キーがNULLになる。

```rb
class Car < ApplicationRecord
  has_many :wheel, dependent: :destroy
end
```

# 会員ブログ関連モデルの準備

## ブログ記事の関連付け

### Entryモデルの作成

- `rails g`コマンドで、Entryモデルを作成する。

```sh
$ 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`を追加する。
    
```rb: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`を実行する。

```sh
$ bin/rails db:migrate
```

### モデル間の関連付け

- Memberモデルにhas_manyメソッドでEntriesと関連付ける。member.id=entry.member_id
- 会員はブログ記事を複数持つ。メンバーは削除されると、記事も削除される。

```rb:app/models/member.rb（一部）
  has_many :entries, dependent: :destroy
```

- 子モデルは、親モデルを定義する。authorメソッドを追加し、本当のモデルであるMemberを指定する。
- 外部キーは、member_idとする。

```rb:app/models/entry.rb
class Entry < ApplicationRecord
  belongs_to :author, class_name: "Member", foreign_key: "member_id"
end
```

## Entryモデルでの準備

### モデルの属性名の日本語化

- 以下のようにモデルの属性名をロケールテキストに追加する。

```yml:config/locales/ja.yml（一部）
ja:
  activerecord:
    models:
#(省略)
      entry: ブログ記事
#(省略)
    attributes:
      entry:
        title: タイトル
        body: 本文
        posted_at: 日時
        status: 状態
        status_draft: 下書き
        status_member_only: 会員限定
        status_public: 公開
```

### バリデーション

- titleは空禁止、200文字以内。
- 本文と投稿日は空禁止。
- 状態は、下書き／会員限定／公開のいずれか。

```rb: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 }
```

### ブログ記事を絞り込むスコープ

- 以下のようにスコープを追加する

```rb: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}
```

### ビュー用のメソッド

```rb: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`テーブルを読み込みする。

```rb:db/seeds.rb（一部）
table_names = %w(members articles entries)
#(省略)
```

- 以下のようなシードデータを追加する。

```rb: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
```

- データベースの再構築を行い、シードデータをセットする。

```sh
bin/rails db:rebuild
```

- データが無事入りました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/ffa031bc-39b9-ea0b-0bda-bbdf3629acfb.png)


参考
[改訂4版 基礎 Ruby on Rails (IMPRESS KISO SERIES)](https://www.amazon.co.jp/%E6%94%B9%E8%A8%824%E7%89%88-Ruby-Rails-IMPRESS-KISO/dp/429500460X/ref=tmm_pap_swatch_0?_encoding=UTF8&qid=1536666358&sr=1-1)