すでにメンバーの場合は

無料会員登録

GitHubアカウントで登録 Pikawakaが許可なくTwitterやFacebookに投稿することはありません。

登録がまだの方はこちらから

Pikawakaにログイン

GitHubアカウントでログイン Pikawakaが許可なくTwitterやFacebookに投稿することはありません。

Rails

【Rails】アソシエーションを図解形式で徹底的に理解しよう!

ぴっかちゃん
ぴっかちゃん

アソシエーションとは、テーブル同士の関連付け(リレーションシップ)をモデル上の関係として操作出来るようにする仕組みのことです。

例えば、以下の画像のようにreviewsテーブルの外部キー制約が設定されたカラム(product_id)の値にproductsテーブルの主キーを格納することで、テーブル同士の関連付けがされて冷蔵庫のレビューを参照したり、レビューがどの商品に紐づいているのか分かるようになります。

冷蔵庫の商品のレビューを参照する

このようなテーブル同士の関連付けで「冷蔵庫のレビュー」を取得する場合は、アソシエーションを定義する事で、直感的なコードによるデータの取得が可能になります。

ちなみにアソシエーションを定義しない場合は、以下のように外部キーのカラムを使う必要があります。

アソシエーションを使用しない場合
1
2
@product = Product.find(1)
@reviews = Review.where(product_id: @product.id)

しかし、アソシエーションを以下のように各モデルクラスに定義する事で、主キーや外部キーなどのテーブル同士の関連付けを直感的に記述する事が出来るのです。

モデルクラス | アソシエーションを定義する
1
2
3
4
5
6
7
8
9
# review.rb
class Review < ActiveRecord::Base
  belongs_to :product
end

# product.rb
class Product < ActiveRecord::Base
  has_many :reviews
end
アソシエーションを使用する場合
1
2
@product = Product.find(1)
@reviews = @product.reviews

このように、アソシエーションを定義する事で簡単にコードを記述する事ができます。アソシエーションの種類はたくさんあるので1つ1つ理解して使いこなしていきましょう。

アソシエーションの事前知識

この章では、アソシエーションを理解するのに必要なリレーションシップやアソシエーションを定義することでコードにどのような変化があるのかを解説します。

リレーションシップとは?

リレーションシップとは、テーブル同士の関連付けのことを指します。
それでは、テーブル同士はどのように関連付けが行われているのでしょうか?

以下の「商品情報を管理するproductsテーブル」と「口コミレビューを管理しているreviewsテーブル」の関連付けを例にして確認します。あるサイトでは、productsテーブルの商品を売っていますが、各商品にはレビューが存在します。このレビューは、どの商品に対してのレビューなのか識別する必要があります。

productsテーブルとreviewsテーブルの構成

この関連付けは、上記の外部キー制約のカラム(reviewsテーブルのproduct_idカラム)にproductsテーブルの主キーの値を入れる事で実現する事ができます。

このように、テーブル同士の関連付けを行う事で冷蔵庫のレビューが分かったり、レビューがどの商品に紐づくかを判断する事が出来ます。

主キーと外部キー制約のカラム

主キーとは、以下のように「テーブルの情報を一意に識別するためのもの」です。

主キーのカラム

そして、外部キー制約のカラムとは、以下のreviewsテーブルのproduct_idカラムのように「他のテーブルのデータを参照する事が出来るように制約を付けたカラムのこと」です。

外部キー制約のカラム

つまり、どの商品にレビューが対応しているのか識別する為に存在します。

外部キー制約をつけたカラムには、カラム名を参照するテーブル名(単数形)_idにして、対応する主キーの値を入れることで参照する事が出来ます。

アソシエーションのコードを比較

先ほどのテーブル関係からid=1の商品(冷蔵庫)に属しているレビューを取得するにはどの様にすれば良いでしょうか?

テーブルからデータを取得する

アソシエーションを使用しない場合と使用した場合でレビューを取得していきます。

アソシエーションを使用しない場合

アソシエーションを特に使用しない場合は、下記の様にproductsテーブルに対応するProductモデルでfindメソッドを利用して、id=1のレコードを取得します。

外部キーの値は主キーの値が入っているので、whereメソッドに外部キーを指定してid=1の商品に属しているレビューを取得します。

アソシエーションを使用しない場合
1
2
@product = Product.find(1)
@reviews = Review.where(product_id: @product.id)

アソシエーションを使用しない場合だと、外部キーを気にしてコードを記述しなければいけません。

アソシエーションを使用する場合

アソシエーションを使用する場合は、「Productオブジェクト.reviews」でその商品に属しているレビューを全て取得する事が出来ます。

アソシエーションを使用する場合
1
2
@product = Product.find(1)
@reviews = @product.reviews

この様にアソシエーションを使用する事で、主キーや外部キーなどのテーブル同士の関連付けを直感的に記述する事が出来るのです。

ER図でテーブル同士の関係性を整理

アソシエーションを使う際に、テーブル同士の関係性を理解している必要があります。テーブル同士の関係性を可視化して整理する為に、ER図(Entity-Relationship Diagram)というものを使います。

usersテーブル、reviewsテーブル, productsテーブルの3つの関係性を例にしてER図で確認してみましょう。

ER図の例

ER図は、IE表記法という書き方で記述されます。
下記の表にある様に3つの記号を使ってテーブル同士の関係性を記述します。

記号 意味
○ (白丸) 0
1
鳥足 (3本に広がる線)

3つのテーブルのER図では、ユーザーと商品の関係は「ユーザー1に対して商品が下限が0で上限が多」という意味になります。つまり、ユーザー1に対しての商品は0以上を表します。

この様にER図を使うと、複数あるテーブル同士の関係を視覚的に整理する事が出来ます。

アソシエーションの種類

この章では、アソシエーションの種類について1つ1つ丁寧に解説します。

belongs_toメソッド

belongs_toメソッドは、参照元テーブル(外部キーを持つテーブル)から参照先テーブル(主キーを持つテーブル)にアクセスする場合に定義します。

belongs_toメソッドが必要な場合とは、どの様な関係性の事なのでしょうか?商品とレビューの関係性を参考にして確認していきましょう。

belongs_toメソッドが必要な関係性とは?

先ほどの商品とレビューの関係性は、下記の図の様に複数のレビューが各商品に必ず属しています。レビューは商品がなければ存在しません。

商品とレビューの関係性

つまり「レビューは必ずどれか1つの商品に属している(Review belongs to a Product)」と言えます。この様な関係性の場合に、belongs_toメソッドを使います。「belong to」は英語で「〜に属する」という意味です。

belongs_toメソッドを定義する

belongs_toメソッドを定義するには、下記のコードの様にモデルクラスに定義します。

モデルクラス | belongs_toメソッドを定義する
1
2
3
class モデル名 < ActiveRecord::Base
  belongs_to :関連名
end

「モデル名はbelongs_toの引数に指定した関連名に属する」という意味になります。

モデル名はbelongs_toの引数に指定した関連名に属する

belongs_toメソッドを定義する事で、参照先(関連する主キーがあるテーブル)の情報が現在のモデルを経由して取得出来る様になります。belongs_toメソッドの引数の関連名は単数形になるので注意してください。

Reviewモデルにbelongs_toメソッドを定義する

先ほどの商品とレビューの「レビューは必ずどれか1つの商品に属している(Review belongs to a Product)」という関係性を定義していきます。

商品のテーブルはproductsテーブル、レビューのテーブルはreviewsテーブルです。reviewsテーブルの外部キーproduct_idで対応するproductsテーブルのレコードを参照します。

productsテーブルとreviewsテーブルの関係

複数のレビューは、1つの商品に属しているので下記の様にbelongs_toメソッドを定義する事が出来ます。

review.rb | belongs_toメソッドを定義する
1
2
3
class Review < ActiveRecord::Base
  belongs_to :product
end

belongs_toメソッドをReviewモデルに定義し引数にproductsテーブルに対応するモデル名を記述します。

has_manyメソッド

先ほどのbelongs_toメソッドの時は、複数のレビューは1つの商品に属すという関係でしたが、商品側から見たらどうなるでしょうか?

商品側から見たreview

商品側からレビューをみると、1つの商品は複数のレビューを持っています(Product has many Reviews)。この様な関係を1:nと言います。商品1つに対してレビューは0以上あるのでnと表現しています。

1:nの関係は、has_manyメソッドを使って定義する事が出来ます。

has_manyメソッドを定義する

has_manyメソッドは、下記のコードの様に1:nの1のモデルクラス側に定義します。

モデルクラス | has_manyメソッドを定義する
1
2
3
class モデル名 < ActiveRecord::Base
  has_many :関連名
end

上記は、「1つのモデルは複数の関連名を持つ」という意味になります。has_manyメソッドの引数の関連名は複数形になるので注意してください。

Productモデルにhas_manyメソッドを定義する

先ほどの「1つの商品は複数のレビューを持つ(Product has many Reviews)」という1:nの関係性をhas_manyメソッドで定義します。

product.rb | has_manyメソッドを定義する
1
2
3
class Product < ActiveRecord::Base
  has_many :reviews
end

上記は、1つのproductオブジェクトに対して、複数のReviewオブジェクトが存在しますという意味になります。複数のオブジェクトなので関連名(reviews)は複数形になっています。

Productモデル経由でReviewモデルの情報を取得する

モデルクラスにhas_manyメソッドを定義したことによって、Productモデル経由でReviewモデルの情報を取得出来る様になりました。

商品id=1(冷蔵庫)のレビューを全て取得する例をみていきましょう。まずは、商品id=1(冷蔵庫)のオブジェクトを取得します。

controller側 | 商品id=1(冷蔵庫)のオブジェクトを取得する
1
2
3
def hasmany
  @product = Product.find(1) # オブジェクトを取得
end

そして、情報を取得するには「オブジェクト.関連名」で関連するオブジェクトを取得する事が出来ます。冷蔵庫に関連するレビューを取得する為に「@product.reviews」を実行すると、複数のレビューのレコードが配列に入って返されます。

has_manyの場合の返り値
1
2
3
4
5
6
@product.reviews

=> #<ActiveRecord::Associations::CollectionProxy [ # 返り値
#<Review id: 2, body: "値段相当で良かった", product_id: 1, created_at: "2019-12-13 05:24:57", updated_at: "2019-12-13 05:24:57">, 
#<Review id: 3, body: "音が結構うるさかった", product_id: 1, created_at: "2019-12-13 05:31:09", updated_at: "2019-12-13 05:31:09">, 
#<Review id: 4, body: "買った時の匂いが気に…", product_id: 1, created_at: "2019-12-13 05:31:52", updated_at: "2019-12-13 05:31:52">]>

これは、1つの商品は複数のレビューを持つというhas_manyの関係なので、返り値は複数のレビューが存在します。

そして、1つの商品である冷蔵庫には複数のレビューが存在している為、下記の様にeachメソッドで1つ1つレビューを取り出します。

view側 | アソシエーションを使って関連するレビューを取得する
1
2
3
4
5
6
<h2>商品名: <%= @product.name %></h2>
<ul>
  <% @product.reviews.each do |review| %> <%# オブジェクト.関連名で取得 %>
    <li><%= review.body %></li>
  <% end %>
</ul>

上記のコードの実行結果は、下記の様に表示されます。

コード実行結果

今回のhas_manyの関係では、1つの商品に関連するレビューを取得すると、複数のレコードが返りましたが、belongs_toの関係である1つのレビューが属している商品を取得する場合の返り値は単一のオブジェクトが返ります。

コンソール | belongs_toの場合の返り値
1
2
3
4
5
review = Review.find(1)
review.product

# 返り値
=> #<Product id: 3, name: "洗濯機", price: "10000", description: "外形寸法:幅608", created_at: "2019-12-13 05:23:40", updated_at: "2019-12-13 05:23:40">

中間テーブルとthroughオプション

pikawakaは、どの様なメンバーと部署で構成されているのか見てみましょう。pikawakaの宮嶋 勇弥さんは、開発部と記事部の責任者です。これは1人のメンバーが複数の部署に属していると言えます。

1人のメンバーが複数の部署に属す

次に開発部の部署を見てみると複数のメンバーがいます。1つの部署には複数のメンバーが属していると言えます。

メンバーと部署の関係

メンバーは複数の部署を持ち、部署も複数のメンバーを持つというお互いに1つに対して複数存在している関係を多対多の関係と呼びます。

多対多をテーブル間のリレーションで表現するには?

先ほどのメンバーと部署の多対多の関係をテーブルで確認します。メンバーのテーブルをusersテーブル、部署のテーブルはdepartmentsテーブルとします。

宮嶋さんは開発と記事の2つの部署に所属しています。一方で、開発部は宮嶋さんと高橋さんが所属しているので、1人のユーザーは複数の部署に所属できるし、1つの部署は複数の人が入れるので、ユーザーと部署は多対多の関係と言えます。

多対多のテーブル

この様な多対多の関係をテーブルにする場合に、中間テーブルというテーブルが必要になります。

belongs_toでは、1つの外部キーを使って参照元テーブルから参照先テーブルを見ていましたが、中間テーブルでは、usersテーブルとdepartmentsテーブルの2つのテーブルをお互いに参照する必要があります。

多対多の関係

それには、中間テーブル(authorizations)に、usersテーブルを参照するための外部キー制約のカラム(user_id)とdepartmentsテーブルを参照するための外部キー制約のカラム(department_id)を持たなければなりません。

中間テーブルが必要な理由

多対多の関係の時に中間テーブルがなくても設計することは可能ですが、カラム数が多くなったり、使われないカラムが出てくるという良くないDB設計になってしまいます。

usersテーブルとdepartmentsテーブルの多対多の関係に中間テーブルを置かない場合は、お互い外部キーを置く必要があります。しかし、このままでは下記の様に1カラムに複数のデータが入ってしまいます。

中間テーブルがない状態のusersテーブルとdepartmentテーブル

原則1カラムには1つのデータしか持つ事が出来ない為、下記の様にカラムを増やして対応しなければなりません。departmentsテーブルの方は、開発部署の人数が増える度にカラムを人数分用意しなければいけなくなります。

カラムが増える例

そして、このままでは下記の様に使用されないカラムが出てきてしまいます。この空カラムにはNULLが入りますが、この様なNULLが沢山入るDB設計はアンチパターンという良くない設計とされます。

アンチパターンのテーブル

部署の人数や1人のメンバーが属する部署が増えれば増える程、未使用のカラムが多い設計となります。

良いDB設計をするためには、カラムにNULLが入らない様な設計にする必要があります。これを実現するのが中間テーブルなのです。中間テーブルを使うと、下記の様に空のカラムは無くなります。

中間テーブルがある場合のusersテーブルとdepartmentsテーブル

中間テーブルを使わない場合のデメリット
  1. カラム数が多くなる
  2. 未使用(NULL)のカラムが多くなる
  3. NULLが沢山入ってしまうアンチパターンという良くない設計になる

多対多の関連を定義する

多対多の関連を定義するには、モデルクラスにhas_manyメソッドのthroughオプションを使います。throughは、「経由する」という意味なので中間テーブルを経由して関連先のオブジェクトを取得することが出来ます。

モデルクラス | has_many throughオプションを定義
1
2
3
4
class モデル名 < ActiveRecord::Base
  has_many :関連名, through: :中間テーブル名
  has_many :中間テーブル名 #おまじない
end

そして3行目にある「has_many :中間テーブル」は、throughオプションを使う時に必要なおまじないだと覚えて置きましょう。

メンバーと部署の多対多を定義する

先ほどのメンバーと部署の多対多の関係をhas_manyメソッドのthroughオプションを使って定義していきます。

まずは、usersテーブルに対応するUserモデルのファイルに定義していきます。下記の「has_many :departments, through: :authorizations」は、「1つのUserオブジェクトはAuthorizationモデルを経由して複数のDepartmentオブジェクトを持っている」という意味になります。

user.rb |has_many throughオプションを定義
1
2
3
4
class User < ActiveRecord::Base
    has_many :departments, through: :authorizations
    has_many :authorizations #おまじない
end

Departmentモデルも同様にhas_manyメソッドのthroughオプションを定義していきます。

department.rb | has_many throughオプションを定義
1
2
3
4
class Department < ActiveRecord::Base
    has_many :users, through: :authorizations
    has_many :authorizations #おまじない
end

authorizationsテーブルの外部キーは、usersテーブルとdepartmentsテーブルを参照するために存在します。その為、中間テーブルであるauthorizationsテーブルに対応するAuthorizationモデルには、belongs_toメソッドを定義します。 (Authorization belongs to User, Authorization belongs to Department)

authorization.rb | belongs_toを定義
1
2
3
4
class Authorization < ActiveRecord::Base
    belongs_to :user
    belongs_to :department
end

そしてauthorizationsテーブルですが、外部キーのカラム以外にもroleというカラムがあります。

authorizationsテーブル

Authorizationは権限という意味です。authorizationsテーブルのroleカラムには、「責任者」か「メンバー」の値が入ります。これは、メンバーの部署での役割が責任者なのか、部署のメンバーという意味になります。

authorizationsテーブルにroleカラムを持たせる理由

roleカラムは、usersテーブルで良さそうですが、usersテーブルにするとメンバーが複数の部署に所属していた場合に役割(role)は1つしか指定出来なくなります。

usersテーブルにroleカラムがある場合に、宮嶋 勇弥さんが開発と記事の部署に所属していて、開発部では責任者の役割があるが、記事部では責任者ではなくメンバーであった時にusersテーブルの宮嶋 勇弥さんの役割(role)には1つしか入れることが出来ないのです。

多対多で定義すると何が出来るのか?

宮嶋 勇弥さん(id=1)の部署のレコードを取得する例を見ていきましょう。

下記のコードは、まずfindメソッドでusersテーブルから宮嶋 勇弥さんのオブジェクトを取得し、「オブジェクト.departments」で宮嶋 勇弥さんの所属する部署のレコードを全て取得しています。

コンソール| id=1の宮嶋 勇弥さんの部署のレコードを全て取得する
1
2
3
4
5
6
7
user = User.find(1) # id=1のメンバーを取得
=> #<User id: 1, name: "宮嶋 勇弥", created_at: "2019-11-22 08:18:59", updated_at: "2019-11-22 08:18:59">

user.departments # id=1のメンバーの部署のレコードを全て取得する
=> #<ActiveRecord::Associations::CollectionProxy
 [#<Department id: 1, name: "開発", created_at: "2019-11-22 08:20:56", updated_at: "2019-11-22 08:20:56">, 
#<Department id: 2, name: "記事" created_at: "2019-11-22 08:21:02", updated_at: "2019-11-22 08:21:02">]>

このコードの実行時の流れは、まず中間テーブルのuser_idを見にいき、user_idが1のレコードのdepartment_id(1と2)を参照してdepartmetsテーブルの1と2のレコードを取得します。

コードが実行される時、中間テーブルのauthorizationsテーブルはあくまでも経由するだけで実際に取得しているのはdepartmentsテーブルのレコードだという事に注意してください。

もちろん部署から部署に属するメンバーも取得することが出来ます。

コンソール| id=1の部署のレコードを全て取得する
1
2
3
4
5
6
7
department = Department.find(1) # id=1の部署を取得
=> #<Department id: 1, name: "開発", created_at: "2019-11-22 08:20:56", updated_at: "2019-11-22 08:20:56">

department.users # id=1の部署に属するメンバーのレコードを全て取得する
=> #<ActiveRecord::Associations::CollectionProxy 
[#<User id: 1, name: "宮嶋 勇弥", created_at: "2019-11-22 08:18:59", updated_at: "2019-11-22 08:18:59">, 
#<User id: 3, name: "高橋大地", created_at: "2019-11-22 08:19:42", updated_at: "2019-11-22 08:19:42">]>

departmentsテーブルからusersテーブルの関連するレコード取得の例
usersテーブルからdepartmentsテーブルのレコードを取得していた時と同様に、コードが実行される時、中間テーブルのauthorizationsテーブルはあくまでも経由するだけです。実際に取得しているのはusersテーブルのレコードです。

SQLで確認してみよう

先ほどの部署に所属するメンバーを取得するコードをもう一度確認してみましょう。

コンソール| id=1の部署のレコードを全て取得する
1
2
3
4
5
6
7
department = Department.find(1) # id=1の部署を取得
=> #<Department id: 1, name: "開発", created_at: "2019-11-22 08:20:56", updated_at: "2019-11-22 08:20:56">

department.users # id=1の部署に属するメンバーのレコードを全て取得する
=> #<ActiveRecord::Associations::CollectionProxy 
[#<User id: 1, name: "宮嶋 勇弥", created_at: "2019-11-22 08:18:59", updated_at: "2019-11-22 08:18:59">,
 #<User id: 3, name: "高橋大地", created_at: "2019-11-22 08:19:42", updated_at: "2019-11-22 08:19:42">]>

実は、4行目の「department.users」では下記の様なSQLが発行されているのです。

department.usersで発行されるSQL文
1
2
3
4
SELECT * FROM `users`
  INNER JOIN `authorizations` 
    ON `users`.`id` = `authorizations`.`user_id` 
      WHERE `authorizations`.`department_id` = 1

このSQLを理解するために、まずはINNER JOINでテーブルが結合されるまでを確認します。(理解しやすい様にSELECT文で全てのカラムを取得する様にしています。)

INNER JOINでテーブルが結合されるまで

上記の画像の様に、INNER JOINは、ON以降に記述された結合条件にしたがってusersテーブルをauthorizatinsテーブルを結合します。

このSQLにWHERE文を追加してみます。このWHERE文は、authorizationsテーブルのuser_idが1のレコードに絞り込みます。

WHERE文を追加

WHERE文によってレコードが絞り込まれたのが分かります。この状態ではusersテーブル以外のauthorizationsテーブルのカラムがまだ含まれていますね。

現在のSELECT文ではテーブル結合を分かりやすくする為に全てのカラム(*)を取得していたので、元のSELECT文に戻して必要なカラムのみを取得します。

SQLのSELECT文を追加

作成されたテーブルは、authorizationsテーブルを通して部署に対応するメンバーのレコードを取得している事が分かりました。今回のコードを再度確認してみましょう。

コンソール| id=1の部署のレコードを全て取得する
1
2
3
4
5
6
7
department = Department.find(1) # id=1の部署を取得
=> #<Department id: 1, name: "開発", created_at: "2019-11-22 08:20:56", updated_at: "2019-11-22 08:20:56">

department.users # id=1の部署に属するメンバーのレコードを全て取得する
=> #<ActiveRecord::Associations::CollectionProxy
[#<User id: 1, name: "宮嶋 勇弥", created_at: "2019-11-22 08:18:59", updated_at: "2019-11-22 08:18:59">, 
#<User id: 3, name: "高橋大地", created_at: "2019-11-22 08:19:42", updated_at: "2019-11-22 08:19:42">]>

「department.users」で関連先のインスタンスを取得出来ていたのは、下記の様なSQLが裏側で発行されていたからでした。

department.usersによって発行されるSQL
1
2
3
4
SELECT * FROM `users`
  INNER JOIN `authorizations`
    ON `users`.`id` = `authorizations`.`user_id`
       WHERE `authorizations`.`department_id` = 1

この様なSQLによって、中間テーブルのアソシエーションを使って関連先のテーブルのインスタンスを取得することが出来るのです。

has_oneメソッド

has_oneメソッドは、1対1の関係を定義します。1対1の関係とは、どのような関係のことなのでしょうか?

例えば、メンバーの宮嶋さんはPikawakaのアカウントを1つ持っています。アカウントを複数持つことはありません。

ユーザーとアカウントの関係

これは、1人のメンバーが1つのアカウントを持っている関係です。この様な関係を1対1の関係と言い、has_oneメソッドで定義する事が出来ます。

1対1をテーブル間のリレーションで表現するには?

1対1の関係をテーブル間で表現するには、参照元テーブルに外部キー制約の付いたカラムを持たせる事で表現することが出来ます。

先ほどのメンバーとアカウントの1対1の関係をテーブルで確認していきましょう。メンバーのテーブルをusersテーブル、アカウントのテーブルをaccountsテーブルとします。

accountsテーブルとusersテーブル

この場合の参照先テーブルがusersテーブル、参照元テーブルがaccountsテーブルなので、accountsテーブルには、外部キー制約の付いたuser_idカラムを持たせる事で参照できる様になります。

has_oneメソッドを定義する

has_oneメソッドを定義するには、主従関係の主にあたるモデルの方に定義します。

モデルクラス| has_oneメソッドを定義する
1
2
3
class モデル名 < ActiveRecord::Base
  has_one :関連名 # 単数形
end

has_oneメソッドは、1対1の関係を定義します。これによって関連付くオブジェクトは1つになるので、関連名は単数形になります。

Userモデルにhas_oneメソッドを定義しよう

それでは、先ほどの例を参考にしてhas_oneメソッドを定義しましょう。
usersテーブルに対応するのはUserモデルになるので、下記の様にhas_oneメソッドを定義します。

user.rb | has_oneメソッドを定義する
1
2
3
class User < ActiveRecord::Base
    has_one :account # 単数形
end

1人のUserは、1つのAccountを持つ関係(User has one account)でした。ユーザーに関連するアカウントは1つなので、関連名は単数形のaccountになります。

そして、accountsテーブルに対応するモデルはAccountモデルになるので、下記の様にbelongs_toメソッドを定義します。

account.rb | belongs_toメソッドを定義する
1
2
3
class Account < ActiveRecord::Base
    belongs_to :user 
end

belongs_toメソッドの引数の関連名は、1つのアカウントは1人のユーザーに属しているので、単数形になります。

ユーザーの持つアカウントを取得してみよう

has_oneメソッドを関連付けしたので、usersテーブルのname='宮嶋勇弥'さんの持つアカウントを取得して、account_numberを表示させます。

accountsテーブルとusersテーブル

まずは、controllerでユーザー情報をfind_byメソッドを使って取得します。

controller側 | name='宮嶋勇弥'のユーザーオブジェクトを取得する
1
2
3
def show
  @user = User.find_by(name: '宮嶋勇弥')
end

ユーザーオブジェクトを取得したら、view側で「オブジェクト.関連名」を指定して関連するオブジェクトを取得します。

view側 | ユーザーの持つaccount_numberを表示する
1
2
<h2><%= @user.name %></h2>
<p><% @user.account.account_number %></p>

has_oneメソッドのアソシエーションは、あるユーザーに関連するアカウントは1つだけなので、「@user.account」@userの持つアカウントを取得することが出来ます。今回はaccount_numberを表示したいので、「@user.account.account_number」の様に記述します。

アソシエーションで利用可能なオプション

この章では、アソシエーションで利用可能なオプションについて解説します。

基本的なテーブルを作成し命名規則に従っていればオプションを利用する事は少ないですが、独自の命名を行っている場合にアソシエーションのオプションを利用する事でカスタマイズする事が出来ます。

この章では、以下の3つのテーブルを使って解説します。

usersテーブルとaccountsテーブルとblogsテーブル

上記をアソシエーション定義すると、以下の通りになります。

user.rb/account.rb/blog.rb | UserモデルとAccountモデルとBlogモデルの関連付け
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# user.rb
class User < ActiveRecord::Base
    has_one :account # 単数形
    has_many :blogs # 複数形
end

# account.rb
class Account < ActiveRecord::Base
    belongs_to :user 
end

# blog.rb
class Blog < ActiveRecord::Base
    belongs_to :user
end

それでは、主要なオプションについて1つ1つ解説します。

foreign_keyオプション

foreign_keyとは、外部キー制約のカラム名を指定することが出来るオプションです。

通常は、外部キー制約のカラム名を「user_id」の様に「参照先のモデル名(小文字) 」+ 「_id」の形式になり外部キーだと認識されます。

外部キー制約の命名

しかし、「参照先のモデル名(小文字) 」+ 「_id」の形式ではなく、違う名前の形式で外部キーに名前をつける場合は、foreign_keyオプションで明示的に宣言する必要があります。

foreign_keyオプションは、下記の様に定義します。

モデルクラス | foreign_keyオプションを定義する
1
2
3
class モデル名 < ActiveRecord::Base
    アソシエーション :関連名, foreign_key: '外部キー名'
end

usersテーブルを参照するaccountsテーブルの外部キー制約のカラム名は「user_id」でした。これを「user_no」にカラム名だけを変更して、usersテーブルのid=1に関連するアカウントを取得してみます。

コンソール | テーブルのカラム名だけを変更して関連するデータを取得
1
2
3
4
5
6
7
8
9
user = User.find(1)
SELECT  `users`.* FROM `users`  WHERE `users`.`id` = 1 LIMIT 1

user.account # 関連するアカウントを取得
SELECT  `accounts`.* FROM `accounts`  WHERE `accounts`.`user_id` = 1 LIMIT 1

# エラー発生
Mysql2::Error: Unknown column 'accounts.user_id' in 'where clause': SELECT  `accounts`.* FROM `accounts`  WHERE `accounts`.`user_id` = 1 LIMIT 1
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'accounts.user_id' in 'where clause': SELECT  `accounts`.* FROM `accounts`  WHERE `accounts`.`user_id` = 1 LIMIT 1

「user.account」を実行した際に、エラーが発生しています。外部キーのカラム名を変更だけを変更しても、デフォルトでは、「テーブル名 + _id」の形式が外部キーだと認識されてしまうので、foreign_keyオプションを追加する必要があります。

foreign_keyオプションを定義する必要性

下記の様に、foreign_keyオプションに変更したカラム名「user_no」を指定します。

user.rb | foreign_keyオプションを追加
1
2
3
class User < ActiveRecord::Base
    has_one :account, foreign_key: 'user_no' # 追加
end

先ほどのコードを実行すると、foreign_keyオプションによって「user_no」が外部キーと認識されて、エラーが起こらずにユーザーに関連するアカウントを取得する事が出来ています。

コンソール | テーブルのカラム名だけを変更後、foreign_keyオプション追加して関連するデータを取得
1
2
3
4
5
6
7
user=User.find(1)
SELECT  `users`.* FROM `users`  WHERE `users`.`id` = 1 LIMIT 1

user.account # 関連するアカウントを取得
  Account Load (14.7ms)  SELECT  `accounts`.* FROM `accounts`  WHERE `accounts`.`user_no` = 1 LIMIT 1

=> #<Account id: 1, account_number: "33344374", user_no: 1, created_at: "2019-12-12 05:46:40", updated_at: "2019-12-12 05:46:40">

この様に、外部キー名を変更してユーザーに関連するアカウントを取得する場合は、Userモデルにforeigh_keyオプションで外部キー名を指定する必要があります。

次に、accountsテーブルのid=1が属しているユーザーを取得してみましょう。

accountsテーブルのid=1が属しているユーザーを取得

コンソール | テーブルのカラム名だけを変更してアカウントに関連するユーザーを取得
1
2
3
account = Account.find(1)
account.user # 関連するユーザーを取得
=> nil

usersテーブルには、id=1のアカウントに関連するレコードが存在しているはずなのに「account.user」を実行すると、nilが返ってきます。

これは、アカウントに関連するユーザーを参照する際にも、foreign_keyオプションでuser_noが外部キー制約のカラムと明示的に宣言する必要があるからです。

試しに、foreign_keyオプションに変更したカラム名「user_no」を指定して先ほどのコードを実行してみます。

account.rb | foreign_keyオプションを追加する
1
2
3
class Account < ActiveRecord::Base
    belongs_to :user, foreign_key: 'user_no' #追加
end
コンソール | account.rbにforeign_keyオプションを追加して、アカウントに関連するユーザーを取得
1
2
3
4
5
6
account = Account.find(1)
account.user # 関連するユーザーを取得
  User Load (2.4ms)  SELECT  `users`.* FROM `users`  WHERE `users`.`id` = 1 LIMIT 1

# 返り値
=> #<User id: 1, name: "宮嶋 勇弥", created_at: "2019-11-22 08:18:59", updated_at: "2019-11-22 08:18:59">

この様に、Accountモデルにforeign_keyオプションで変更した外部キー名を明示的に宣言することによって、アカウントに関連するユーザーを取得することが出来ました。

ポイント
  1. foreign_keyとは、外部キー制約のカラム名を指定することが出来るオプションのことです。
  2. デフォルトでは、「テーブル名 + _id」の形式が外部キーだと認識されてしまいます。
  3. 外部キー制約のカラム名を変更する場合は、カラム名だけではなくforign_keyオプションで各モデルに宣言する必要があります。

class_nameオプション

class_nameとは、関連名を変更する為に、クラス名を明示的に宣言するオプションです。

関連名とは、関連するテーブルを参照する際のメソッド名になります。このメソッド名を変更するためには、class_nameオプションで関連するモデルのクラス名を指定する必要があります。

class_nameオプションは、下記の様に定義します。

モデルクラス | class_nameオプションを定義する
1
2
3
class モデル名 < ActiveRecord::Base
    アソシエーション :変更したい関連名, class_name: '関連するモデルのクラス名'
end

usersテーブルとhas one関係にあるaccountsテーブルの例を見ながら確認していきます。usersテーブルのid=1(宮嶋さん)に関連するaccountsテーブルのデータを参照するには、下記の様にUserモデルのhas_oneのアソシエーションで指定した関連名(account)で参照する事が出来ます。

usersテーブルとaccountsテーブルの関連

controller側
1
@user = User.find(1)
view側
1
<p><%= @user.account %><p>

accountの関連名ではなく、違う名前を使ってアカウントを参照する場合は、下記の様にclass_nameオプションを使って定義します。

user.rb | UserモデルとAccountモデルの関連付け
1
2
3
class User < ActiveRecord::Base
    has_one :pikawaka_account, class_name: 'Account'
end

これによって「@user.account」ではなく、「@user.pikawaka_account」で関連するアカウントを参照する事が出来る様になりました。

view側 | pikawaka_accountで関連するaccountsテーブルを参照
1
<p><%= @user.pikawaka_account %><p>

foreign_keyオプションとclass_nameオプション

foreign_keyオプションとclass_nameオプションは、併用して使うことが出来ます。2つのオプションを併用しなければいけない場面とはどの様な場面なのでしょうか?

それでは、「pikawakaのメンバーを管理するusersテーブル」と「pikawakaの記事を管理するblogsテーブル」を例にして確認していきます。2つのテーブルと定義されているアソシエーションは下記の通りでした。

usersテーブルとblogsテーブル

user.rb/blog.rb | UserモデルとBlogモデルの関連付け
1
2
3
4
5
6
7
8
9
# user.rb
class User < ActiveRecord::Base
    has_many :blogs # 複数形
end

# blog.rb
class Blog < ActiveRecord::Base
    belongs_to :user
end

このアソシエーションによって、pikawakaのメンバーの宮嶋勇弥さん(usersテーブルのid=1)の書いた記事を全て取得することが出来ます。記事を書いた人を取得する事も出来ます。

usersテーブルのid=1のブログ

コンソール | 宮嶋勇弥さんの記事を全て取得する
1
2
3
4
5
6
7
user = User.find(1)
user.blogs
SELECT `blogs`.* FROM `blogs`  WHERE `blogs`.`user_id` = 1

=> #<ActiveRecord::Associations::CollectionProxy [ # 返り値
#<Blog id: 1, title: "rubyとは?", user_id: 1, created_at: nil, updated_at: nil>, 
#<Blog id: 2, title: "bundlerとは?", user_id: 1, created_at: nil, updated_at: nil>]>

pikawakaでは、1つの記事に対して必ずレビューをする担当者が1人います。
しかし、現在のblogsテーブルの構成ではblogsテーブルのid=1(rubyとは?)の記事を書いた人が宮嶋さんというデータしか取得する事が出来ません。

現在のblogsテーブルの状況

記事のレビューをした人を取得するには、blogsテーブルに記事を書いた人と記事をレビューする人を参照することが出来るように、外部キー制約の付いたカラムを2つ置く必要があります。

blogsテーブルに外部キーを2つ置く例

このようにすると、blogsテーブルのid=1(rubyとは?)の記事を書いた人が宮嶋さん、レビューをした人は高橋さんということが分かります。また、下記のようにusersテーブルのid=1(宮嶋さん)の書いた記事を全て取得する事や、レビューした記事を全て取得する事が出来ます。

id=1のレビューした記事や書いた記事を取得する例

しかし、外部キー制約のカラム名は「user_id」の様に「参照先のモデル名(小文字) 」+ 「_id」の形式が外部キーだと認識されます。(※ 詳しくはforeign_keyオプションを参照)

ここでは、分かりやすくする為にデフォルトの外部キー名user_idを使わず「記事を書いた人(外部キー1)」のカラム名をwriter_id、「レビューをした人(外部キー2)」のカラム名をreviewer_idにします。

外部キー1、外部キー2のカラム名を変更

しかし、このままではwriter_idとreviewer_idは外部キーのカラムだと認識されないので、下記の様にUserモデルとBlogモデルのアソシエーションにforegin_keyオプションを使ってwriter_idとreviewer_idが外部キーだと明示的に宣言する必要があります。

user.rb/blog.rb | writer_idとreviewer_idをforeign_keyを使って外部キーと宣言する
1
2
3
4
5
6
7
8
9
10
11
# user.rb
class User < ActiveRecord::Base
    has_many :blogs, foreign_key: 'writer_id' #追加
    has_many :blogs, foreign_key: 'reviewer_id' #追加
end

# blog.rb
class Blog < ActiveRecord::Base
    belongs_to :user, foreign_key: 'writer_id' #追加
    belongs_to :user, foreign_key: 'reviewer_id' #追加
end

foreign_keyオプションで定義したことによって、writer_idとreviewer_idが外部キーと認識されましたが、下記の様にユーザーに関連する記事にアクセスする際に:blogsの関連名が同一になっているので、書いた記事を取得したいのか、レビューした記事を取得したいのか判断することが出来ません。

関連名が同一になっている例

この状態でUserモデルのインスタンスに関連する記事を取得しようするとエラーが起こります。(Blogモデルでも関連名の:userが同じ為エラーが起こります。)

usersテーブルのid=1の記事を全て取得する
1
2
3
user = User.find(1)
user.blogs
Mysql2::Error: Unknown column 'blogs.reviewer_id' in 'where clause': SELECT `blogs`.* FROM `blogs`  WHERE `blogs`.`reviewer_id` = 1

どちらの記事を取得するのか、記事を書いた人なのかレビューした人なのか判断する為にclass_nameオプションを利用して、同一の関連名を変更します。

元の関連名(:blogs)からどんな記事なのか分かるように、書いた記事にアクセスする関連名を: wrote_blogs、レビューした記事にアクセスする関連名を:reviewed_blogsに変更します。

user.rb | class_nameオプションで関連名をそれぞれ変更する
1
2
3
4
class User < ActiveRecord::Base
    has_many :wrote_blogs, class_name: 'Blog', foreign_key: 'writer_id' #class_name追加
    has_many :reviewed_blogs,  class_name: 'Blog', foreign_key: 'reviewer_id' #class_name追加
end

class_nameオプションで関連名を変更したことによって、エラーなくユーザーに関連する書いた記事とレビューした記事を区別してアクセスすることが出来る様になりました。下記はid=1の宮嶋勇弥さんの書いた記事とレビューした記事を取得する例です。

宮嶋勇弥さんの書いた記事とレビューした記事を取得する
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
user = User.find(1)
=> #<User id: 1, name: "宮嶋勇弥", created_at: nil, updated_at: nil>

# レビューした記事を取得する
user.reviewed_blogs
SELECT `blogs`.* FROM `blogs`  WHERE `blogs`.`reviewer_id` = 1

=> #<ActiveRecord::Associations::CollectionProxy [ # 返り値
#<Blog id: 3, title: "sketchの使い方", writer_id: 2, reviewer_id: 1, created_at: nil, updated_at: nil>]>

# 書いた記事を取得する
user.wrote_blogs
SELECT `blogs`.* FROM `blogs`  WHERE `blogs`.`writer_id` = 1

=> #<ActiveRecord::Associations::CollectionProxy [ # 返り値
#<Blog id: 1, title: "rubyとは?", writer_id: 1, reviewer_id: 3, created_at: nil, updated_at: nil>, 
#<Blog id: 2, title: "bundlerとは?", writer_id: 1, reviewer_id: 3, created_at: nil, updated_at: nil>]>

この様に関連する記事が書いた記事なのか、レビューした記事なのか判断して複数のデータを取得することが出来ています。

そして、Blogモデルも記事を書いた人か、記事をレビューした人かを区別させる為に、class_nameオプションを使います。下記の様に書いた人にアクセスする関連名を:writer, レビューした人にアクセスする関連名を:reviewerに変更します。

blog.rb | class_nameオプションで関連名をそれぞれ変更する
1
2
3
4
5
# blog.rb
class Blog < ActiveRecord::Base
    belongs_to :writer, class_name: 'User', foreign_key: 'writer_id' #追加
    belongs_to :reviewer, class_name: 'User', foreign_key: 'reviewer_id' #追加
end

下記は、blogsテーブルのid=1の「rubyとは?」の記事を書いた人とレビューした人を取得しています。1つの記事は1人のユーザーに属しているという関係(Blog belogs to User)なので返り値が複数ではなく、Userモデルのインスタンスを返しているという点にも注目してください。

コンソール | rubyとは?の記事を書いた人とレビューした人を取得する
1
2
3
4
5
6
7
8
9
10
11
12
blog = Blog.find(1)
=> #<Blog id: 1, title: "rubyとは?", writer_id: 1, reviewer_id: 3, created_at: nil, updated_at: nil>

# 記事を書いた人を取得する
blog.writer
SELECT  `users`.* FROM `users`  WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, name: "宮嶋勇弥", created_at: nil, updated_at: nil>

# 記事をレビューした人を取得する
blog.reviewer
SELECT  `users`.* FROM `users`  WHERE `users`.`id` = 3 LIMIT 1
=> #<User id: 3, name: "高橋大地", created_at: "2019-11-22 08:19:54", updated_at: "2019-11-22 08:19:54">

この様にclass_nameオプションとforegin_keyオプションを併用して使うことによって、1つのUserモデルに対して複数の役割を持たせて参照することが出来ます。

dependentオプション

dependentとは、親モデルを削除する時に関連付けされている子モデルの挙動を決めるオプションで、下記の様に定義します。

モデルクラス | dependentオプションの定義する
1
2
3
class モデル名 < ActiveRecord::Base
    アソシエーション :関連名, dependent: :オプションの種類
end

下記は、dependentオプションの主な種類です。

オプションの種類 説明 指定したモデルの削除時の挙動 補足説明
:destroy 関連するレコードも一緒に削除する destoryを実行 関連するモデルのコールバックが実行される
:delete 関連するレコードも一緒に削除する deleteを実行 直接データベースから削除されるので、コールバックは実行されない
:delete_all 関連するレコードも一緒に削除する deleteを実行 :deleteと挙動は同じだが、has_manyの場合は、:delete_allを使う
:nullify 関連するレコードを削除しない 外部キーにnullが入れられる nullが入る事によって、レコードを残しつつ関連は解消させる。コールバックは実行されない

destoryメソッドやdeleteメソッドとの違いは、【Rails】destoryメソッドの使い方を徹底解説!を確認してください。

親モデルを削除する際に、dependentオプションで関連付けされている子モデルに対しての挙動を決めておかないとエラーが起こります。

例えば、下記の様にusersテーブルのid=1の宮嶋さんを削除した時にblogsテーブルに宮嶋さんの記事(id=1,2)が残ってしまうと、アソシエーションで blog.user.name とした際にNoMethodError: undefined method 'name' for nil:NilClass のエラーが起こります。

usersテーブルのid=1のデータを削除

コンソール | 記事のユーザーが存在していない場合の挙動
1
2
3
4
5
6
7
8
9
User.first.destroy # usersテーブルのid=1のレコードを削除
DELETE FROM `users` WHERE `users`.`id` = 1

blog = Blog.first # blogsテーブルのid=1のレコードを取得
SELECT  `blogs`.* FROM `blogs`   ORDER BY `blogs`.`id` ASC LIMIT 1
=> #<Blog id: 1, title: "rubyとは?", user_id: 1, created_at: "2020-01-10 03:22:20", updated_at: "2020-01-10 03:22:20">

blog.user.name # 記事を書いたユーザーの名前を取得する
NoMethodError: undefined method `name' for nil:NilClass # エラーが発生する

これは、blogsテーブルのid=1,2のuser_idカラムの値が、usersテーブルに存在しないidなので、blog.userとしてもnilが返るためです。この様な自体を防ぐためにも関連する子モデルにはdependent: :destroyをつけておく方が安全です。

今回は関連するblogsテーブルのレコードを保持しておく必要がないので、:destroydependentオプションに指定します。親モデルがUserで、子モデルがBlogになるので、Userモデルのアソシエーションにdependent: :destroyを追加します。

user.rb / blog.rb | UserモデルとBlogモデルの関連付け
1
2
3
4
5
6
7
8
9
# user.rb
class User < ActiveRecord::Base
    has_many :blogs, dependent: :destroy # オプション追加
end

# blog.rb
class Blog < ActiveRecord::Base
    belongs_to :user 
end

dependent: :destroyが追加された状態で、usersテーブルのid=1(宮嶋さん)のレコードを削除します。

コンソール | dependent: :destroyオプションを追加した際の挙動を確認
1
2
3
4
5
6
7
8
9
10
11
user = User.find(1) # usersテーブルのid=1のデータを取得
SELECT  `users`.* FROM `users`  WHERE `users`.`id` = 1 LIMIT 1

user.destroy # id=1のレコードを削除
   (0.2ms)  BEGIN
  SELECT `blogs`.* FROM `blogs`  WHERE `blogs`.`user_id` = 1
  SQL (1.3ms)  DELETE FROM `blogs` WHERE `blogs`.`id` = 1 # 削除
  SQL (0.4ms)  DELETE FROM `blogs` WHERE `blogs`.`id` = 2 # 削除
  SQL (0.3ms)  DELETE FROM `users` WHERE `users`.`id` = 1 # 削除
   (30.7ms)  COMMIT
=> #<User id: 1, name: "宮嶋 勇弥", created_at: "2019-11-22 08:18:59", updated_at: "2019-11-22 08:18:59">

user.destroyを実行すると、下記のレコードを削除するという3つのDELETE文が発行されます。先ほど解説した親モデルを削除した際に、子モデルに親モデルの外部キーが残ってる限り削除できないというエラー(ActiveRecord::StatementInvalid: Mysql2::Error: Cannot delete or update a parent row: a foreign key constraint fails)が起こらないように、子モデル(Blog)のデータから削除されています。

  • blogsテーブルのid=1
  • blogsテーブルのid=2
  • usersテーブルのid=1

usersテーブルのid=1のデータと関連する2つの記事のデータが削除されてから、ユーザーのデータが削除されている事が確認出来ました。

また親モデルのUserのアソシエーションではなく、子モデルのBlogのアソシエーションにdependent: :destroyを追加してしまうと、記事を削除した際に記事を書いたユーザーも削除されるので注意してください。ユーザーが書いた記事は削除した1記事だけではなく他にも存在するのでエラーが起こるので注意してください。

この記事のまとめ

  • アソシエーションは、テーブル同士の関連付けをモデル上の関係として操作する事が出来る
  • belongs_toメソッドは、他のモデルに属している事を定義する
  • has_manyメソッドは、他のモデルを複数持っている事を定義する
  • throughオプションは、他のモデルと多対多の関係の繋がりを定義する

13

わかった!