更新日:
【Rails】 アソシエーションを図解形式で徹底的に理解しよう!
アソシエーションとは、テーブル同士の関連付け(リレーションシップ)をモデル上の関係として操作出来るようにする仕組みのことです。
例えば、次のようなテーブル同士の関連付けがあるとします。
reviewsテーブルのproduct_idカラム(外部キー制約)にproductsテーブルの主キー(一意の値)を格納することによって、どの商品とレビューが紐づいているのかわかります。
このようなテーブル同士の関連付けでアソシエーションなしで「冷蔵庫のレビュー」を取得する場合は、次のようにreviewsテーブルのproduct_idカラムを使う必要があります。
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 # 直感的なコードになる
このようにアソシエーションを定義すると、テーブル同士の関連付けをモデル同士の関係として直感的なコードで操作できるようになります。
アソシエーションの事前知識
この章では、アソシエーションを理解するのに必要なリレーションシップやアソシエーションを定義することでコードにどのような変化があるのかを解説します。
リレーションシップとは?
リレーションシップとは、テーブル同士の関連付けのことを指します。
例えば、あるサイトで商品を売っていますが、各商品にはレビューが存在します。このレビューは、どの商品に対してのレビューなのか識別する必要があります。
このような場合に関連付けが必要になりますが、この関連付けは次のように外部キー制約のカラム(product_idカラム)にproductsテーブルの主キーの値を格納すれば実現することができます。
このように、テーブル同士の関連付けで冷蔵庫のレビューが分かったり、レビューがどの商品に紐づくかを判断することができます。
主キーと外部キー制約のカラム
主キーとは、「テーブルの情報を一意に識別するためのもの」です。
そして、外部キー制約のカラムとは「他のテーブルのデータを参照する事が出来るように制約を付けたカラムのこと」です。
つまり、どの商品にレビューが対応しているのか識別する為に存在します。
外部キー制約をつけたカラムには、カラム名を参照するテーブル名(単数形)_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図は、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メソッドを定義するには、下記のコードの様にモデルクラスに定義します。
1
2
3
class モデル名 < ActiveRecord::Base
belongs_to :関連名
end
「モデル名はbelongs_toの引数に指定した関連名に属する」という意味になります。
belongs_toメソッドを定義する事で、参照先(関連する主キーがあるテーブル)の情報が現在のモデルを経由して取得出来る様になります。belongs_toメソッドの引数の関連名は単数形になるので注意してください。
Reviewモデルにbelongs_toメソッドを定義する
先ほどの商品とレビューの「レビューは必ずどれか1つの商品に属している(Review belongs to a Product)」という関係性を定義していきます。
商品のテーブルはproductsテーブル、レビューのテーブルはreviewsテーブルです。reviewsテーブルの外部キーproduct_idで対応するproductsテーブルのレコードを参照します。
複数のレビューは、1つの商品に属しているので下記の様にbelongs_toメソッドを定義する事が出来ます。
1
2
3
class Review < ActiveRecord::Base
belongs_to :product
end
belongs_toメソッドをReviewモデルに定義し引数にproductsテーブルに対応するモデル名を記述します。
has_manyメソッド
先ほどのbelongs_toメソッドの時は、複数のレビューは1つの商品に属すという関係でしたが、商品側から見たらどうなるでしょうか?
商品側からレビューをみると、1つの商品は複数のレビューを持っています(Product has many Reviews)。この様な関係を1:nと言います。商品1つに対してレビューは0以上あるのでnと表現しています。
1:nの関係は、has_manyメソッドを使って定義する事が出来ます。
has_manyメソッドを定義する
has_manyメソッドは、下記のコードの様に1:nの1のモデルクラス側に定義します。
1
2
3
class モデル名 < ActiveRecord::Base
has_many :関連名
end
上記は、「1つのモデルは複数の関連名を持つ」という意味になります。has_manyメソッドの引数の関連名は複数形になるので注意してください。
Productモデルにhas_manyメソッドを定義する
先ほどの「1つの商品は複数のレビューを持つ(Product has many Reviews)」という1:nの関係性を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(冷蔵庫)のオブジェクトを取得します。
1
2
3
def hasmany
@product = Product.find(1) # オブジェクトを取得
end
そして、情報を取得するには「オブジェクト.関連名」で関連するオブジェクトを取得する事が出来ます。冷蔵庫に関連するレビューを取得する為に「@product.reviews」を実行すると、複数のレビューのレコードが配列に入って返されます。
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つレビューを取り出します。
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つのレビューが属している商品を取得する場合の返り値は単一のオブジェクトが返ります。
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つに対して複数存在している関係を多対多の関係と呼びます。
多対多をテーブル間のリレーションで表現するには?
先ほどのメンバーと部署の多対多の関係をテーブルで確認します。メンバーのテーブルをusersテーブル、部署のテーブルはdepartmentsテーブルとします。
宮嶋さんは開発と記事の2つの部署に所属しています。一方で、開発部は宮嶋さんと高橋さんが所属しているので、1人のユーザーは複数の部署に所属できるし、1つの部署は複数の人が入れるので、ユーザーと部署は多対多の関係と言えます。
この様な多対多の関係をテーブルにする場合に、中間テーブルというテーブルが必要になります。
belongs_toでは、1つの外部キーを使って参照元テーブルから参照先テーブルを見ていましたが、中間テーブルでは、usersテーブルとdepartmentsテーブルの2つのテーブルをお互いに参照する必要があります。
それには、中間テーブル(authorizations)に、usersテーブルを参照するための外部キー制約のカラム(user_id)とdepartmentsテーブルを参照するための外部キー制約のカラム(department_id)を持たなければなりません。
中間テーブルが必要な理由
多対多の関係の時に中間テーブルがなくても設計することは可能ですが、カラム数が多くなったり、使われないカラムが出てくるという良くないDB設計になってしまいます。
usersテーブルとdepartmentsテーブルの多対多の関係に中間テーブルを置かない場合は、お互い外部キーを置く必要があります。しかし、このままでは下記の様に1カラムに複数のデータが入ってしまいます。
原則1カラムには1つのデータしか持つ事が出来ない為、下記の様にカラムを増やして対応しなければなりません。departmentsテーブルの方は、開発部署の人数が増える度にカラムを人数分用意しなければいけなくなります。
そして、このままでは下記の様に使用されないカラムが出てきてしまいます。この空カラムにはNULL
が入りますが、この様なNULL
が沢山入るDB設計はアンチパターンという良くない設計とされます。
部署の人数や1人のメンバーが属する部署が増えれば増える程、未使用のカラムが多い設計となります。
良いDB設計をするためには、カラムにNULLが入らない様な設計にする必要があります。これを実現するのが中間テーブルなのです。中間テーブルを使うと、下記の様に空のカラムは無くなります。
多対多の関連を定義する
多対多の関連を定義するには、モデルクラスにhas_manyメソッドのthroughオプションを使います。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オブジェクトを持っている」という意味になります。
1
2
3
4
class User < ActiveRecord::Base
has_many :departments, through: :authorizations
has_many :authorizations #おまじない
end
Departmentモデルも同様に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)
1
2
3
4
class Authorization < ActiveRecord::Base
belongs_to :user
belongs_to :department
end
そしてauthorizationsテーブルですが、外部キーのカラム以外にもroleというカラムがあります。
Authorizationは権限という意味です。authorizationsテーブルのroleカラムには、「責任者」か「メンバー」の値が入ります。これは、メンバーの部署での役割が責任者なのか、部署のメンバーという意味になります。
authorizationsテーブルにroleカラムを持たせる理由
roleカラムは、usersテーブルで良さそうですが、usersテーブルにするとメンバーが複数の部署に所属していた場合に役割(role)は1つしか指定出来なくなります。
usersテーブルにroleカラムがある場合に、宮嶋 勇弥さんが開発と記事の部署に所属していて、開発部では責任者の役割があるが、記事部では責任者ではなくメンバーであった時にusersテーブルの宮嶋 勇弥さんの役割(role)には1つしか入れることが出来ないのです。
多対多で定義すると何が出来るのか?
宮嶋 勇弥さん(id=1)の部署のレコードを取得する例を見ていきましょう。
下記のコードは、まずfindメソッドでusersテーブルから宮嶋 勇弥さんのオブジェクトを取得し、「オブジェクト.departments」で宮嶋 勇弥さんの所属する部署のレコードを全て取得しています。
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テーブルのレコードだという事に注意してください。
もちろん部署から部署に属するメンバーも取得することが出来ます。
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">]>
usersテーブルからdepartmentsテーブルのレコードを取得していた時と同様に、コードが実行される時、中間テーブルのauthorizationsテーブルはあくまでも経由するだけです。実際に取得しているのはusersテーブルのレコードです。
SQLで確認してみよう
先ほどの部署に所属するメンバーを取得するコードをもう一度確認してみましょう。
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が発行されているのです。
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は、ON以降に記述された結合条件にしたがってusersテーブルをauthorizatinsテーブルを結合します。
このSQLにWHERE文を追加してみます。このWHERE文は、authorizationsテーブルのuser_idが1のレコードに絞り込みます。
WHERE文によってレコードが絞り込まれたのが分かります。この状態ではusersテーブル以外のauthorizationsテーブルのカラムがまだ含まれていますね。
現在のSELECT文ではテーブル結合を分かりやすくする為に全てのカラム(*)を取得していたので、元のSELECT文に戻して必要なカラムのみを取得します。
作成されたテーブルは、authorizationsテーブルを通して部署に対応するメンバーのレコードを取得している事が分かりました。今回のコードを再度確認してみましょう。
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が裏側で発行されていたからでした。
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テーブルとします。
この場合の参照先テーブルがusersテーブル、参照元テーブルがaccountsテーブルなので、accountsテーブルには、外部キー制約の付いたuser_idカラムを持たせる事で参照できる様になります。
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メソッドを定義します。
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メソッドを定義します。
1
2
3
class Account < ActiveRecord::Base
belongs_to :user
end
belongs_toメソッドの引数の関連名は、1つのアカウントは1人のユーザーに属しているので、単数形になります。
ユーザーの持つアカウントを取得してみよう
has_oneメソッドを関連付けしたので、usersテーブルのname='宮嶋勇弥'さんの持つアカウントを取得して、account_numberを表示させます。
まずは、controllerでユーザー情報をfind_byメソッドを使って取得します。
1
2
3
def show
@user = User.find_by(name: '宮嶋勇弥')
end
ユーザーオブジェクトを取得したら、view側で「オブジェクト.関連名」を指定して関連するオブジェクトを取得します。
1
2
<h2><%= @user.name %></h2>
<p><% @user.account.account_number %></p>
has_oneメソッドのアソシエーションは、あるユーザーに関連するアカウントは1つだけなので、「@user.account」@userの持つアカウントを取得することが出来ます。今回はaccount_numberを表示したいので、「@user.account.account_number」の様に記述します。
モデル同士のリレーションについては、こちらの書籍でさらに詳しく解説されています!Railsについて理解が曖昧だという方にも最適な良書です。
アソシエーションで利用可能なオプション
この章では、アソシエーションで利用可能なオプションについて解説します。
基本的なテーブルを作成し命名規則に従っていればオプションを利用する事は少ないですが、独自の命名を行っている場合にアソシエーションのオプションを利用する事でカスタマイズする事が出来ます。
この章では、以下の3つのテーブルを使って解説します。
上記をアソシエーション定義すると、以下の通りになります。
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オプションは、下記の様に定義します。
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オプションに変更したカラム名「user_no」を指定します。
1
2
3
class User < ActiveRecord::Base
has_one :account, foreign_key: 'user_no' # 追加
end
先ほどのコードを実行すると、foreign_keyオプションによって「user_no」が外部キーと認識されて、エラーが起こらずにユーザーに関連するアカウントを取得する事が出来ています。
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が属しているユーザーを取得してみましょう。
1
2
3
account = Account.find(1)
account.user # 関連するユーザーを取得
=> nil
usersテーブルには、id=1のアカウントに関連するレコードが存在しているはずなのに「account.user」を実行すると、nilが返ってきます。
これは、アカウントに関連するユーザーを参照する際にも、foreign_keyオプションでuser_noが外部キー制約のカラムと明示的に宣言する必要があるからです。
試しに、foreign_keyオプションに変更したカラム名「user_no」を指定して先ほどのコードを実行してみます。
1
2
3
class Account < ActiveRecord::Base
belongs_to :user, foreign_key: 'user_no' #追加
end
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オプションで変更した外部キー名を明示的に宣言することによって、アカウントに関連するユーザーを取得することが出来ました。
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)で参照する事が出来ます。
1
@user = User.find(1)
1
<p><%= @user.account %><p>
accountの関連名ではなく、違う名前を使ってアカウントを参照する場合は、下記の様にclass_nameオプションを使って定義します。
1
2
3
class User < ActiveRecord::Base
has_one :pikawaka_account, class_name: 'Account'
end
これによって「@user.account」ではなく、「@user.pikawaka_account」で関連するアカウントを参照する事が出来る様になりました。
1
<p><%= @user.pikawaka_account %><p>
foreign_keyオプションとclass_nameオプション
foreign_keyオプションとclass_nameオプションは、併用して使うことが出来ます。2つのオプションを併用しなければいけない場面とはどの様な場面なのでしょうか?
それでは、「pikawakaのメンバーを管理するusersテーブル」と「pikawakaの記事を管理するblogsテーブル」を例にして確認していきます。2つのテーブルと定義されているアソシエーションは下記の通りでした。
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)の書いた記事を全て取得することが出来ます。記事を書いた人を取得する事も出来ます。
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テーブルに記事を書いた人と記事をレビューする人を参照することが出来るように、外部キー制約の付いたカラムを2つ置く必要があります。
このようにすると、blogsテーブルのid=1(rubyとは?)の記事を書いた人が宮嶋さん、レビューをした人は高橋さんということが分かります。また、下記のようにusersテーブルのid=1(宮嶋さん)の書いた記事を全て取得する事や、レビューした記事を全て取得する事が出来ます。
しかし、外部キー制約のカラム名は「user_id」の様に「参照先のモデル名(小文字) 」+ 「_id」の形式が外部キーだと認識されます。(※ 詳しくはforeign_keyオプションを参照)
ここでは、分かりやすくする為にデフォルトの外部キー名user_idを使わず「記事を書いた人(外部キー1)」のカラム名をwriter_id、「レビューをした人(外部キー2)」のカラム名をreviewer_idにします。
しかし、このままではwriter_idとreviewer_idは外部キーのカラムだと認識されないので、下記の様にUserモデルとBlogモデルのアソシエーションにforegin_keyオプションを使ってwriter_idとreviewer_idが外部キーだと明示的に宣言する必要があります。
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が同じ為エラーが起こります。)
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に変更します。
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に変更します。
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モデルのインスタンスを返しているという点にも注目してください。
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とは、親モデルを削除する時に関連付けされている子モデルの挙動を決めるオプションで、下記の様に定義します。
1
2
3
class モデル名 < ActiveRecord::Base
アソシエーション :関連名, dependent: :オプションの種類
end
下記は、dependentオプションの主な種類です。
オプションの種類 | 説明 | 指定したモデルの削除時の挙動 | 補足説明 |
---|---|---|---|
:destroy | 関連するレコードも一緒に削除する | destroyを実行 | 関連するモデルのコールバックが実行される |
:delete | 関連するレコードも一緒に削除する | deleteを実行 | 直接データベースから削除されるので、 コールバックは実行されない |
:delete_all | 関連するレコードも一緒に削除する | deleteを実行 | :deleteと挙動は同じだが、 has_manyの場合は、:delete_allを使う |
:nullify | 関連するレコードを削除しない | 外部キーにnullが入れられる | nullが入る事によって、レコードを残しつつ関連は解消させる。 コールバックは実行されない |
親モデルを削除する際に、dependent
オプションで関連付けされている子モデルに対しての挙動を決めておかないとエラーが起こります。
例えば、下記の様にusersテーブルのid=1の宮嶋さんを削除した時にblogsテーブルに宮嶋さんの記事(id=1,2)が残ってしまうと、アソシエーションで blog.user.name
とした際にNoMethodError: undefined method 'name' for nil:NilClass
のエラーが起こります。
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テーブルのレコードを保持しておく必要がないので、:destroy
をdependent
オプションに指定します。親モデルがUserで、子モデルがBlogになるので、Userモデルのアソシエーションにdependent: :destroy
を追加します。
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
(宮嶋さん)のレコードを削除します。
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オプションは、他のモデルと多対多の関係の繋がりを定義する