モデルのアソシエーション
Railsにおいて、正規化を用いてモデル間の関係をシンプルに実現する仕組みをアソシエーションという。
モデルの関係
モデルの関係は、以下の通りにわけることができる。
- 1対多(has_many)
- 1対1(has_one)
- 所属関係(belongs_to)
- 多対多
has_many/belongs_to
以下のモデル構成で具体例をみていく。
managementアプリケーションを作成し、ProjectManagerモデルを生成する。
% bin/rails g model project_manager name:string Running via Spring preloader in process 3956 invoke active_record create db/migrate/20201014131955_create_project_managers.rb create app/models/project_manager.rb invoke test_unit create test/models/project_manager_test.rb create test/fixtures/project_managers.yml
Projectモデルを生成する。
% bin/rails g model project name:string project_manager:references Running via Spring preloader in process 3982 invoke active_record create db/migrate/20201014132028_create_projects.rb create app/models/project.rb invoke test_unit create test/models/project_test.rb create test/fixtures/projects.yml
references型を利用すると、子モデル側にbelongs_toのアソシエーションも自動生成してくれる。
# app/models/project.rb class Project < ApplicationRecord belongs_to :project_manager end
Memberモデルを生成する。
% bin/rails g model member name:string project:references Running via Spring preloader in process 4004 invoke active_record create db/migrate/20201014132111_create_members.rb create app/models/member.rb invoke test_unit create test/models/member_test.rb create test/fixtures/members.yml
has_manyメソッドは自動で設定されないため、ProjectManagerモデルとProjectモデルを以下のように修正する。
# app/models/project_manager.rb class ProjectManager < ApplicationRecord has_many :projects end
# app/models/project.rb class Project < ApplicationRecord belongs_to :project_manager has_many :members end
ちなみにMemberモデルは以下の内容。
# app/models/member.rb class Member < ApplicationRecord belongs_to :project end
db:migrateしてRailsコンソールで動作確認する。
ローカル変数pmとして新規のプロジェクトマネージャーのインスタンスを作成する。createメソッドを使用しているためデータベーステーブルにも登録される。インスタンスを作成するだけの場合はcreateではなくnew(またはbuild)を使用する。
irb(main):001:0> pm = ProjectManager.create(name: 'Taro Yamada') (0.7ms) SELECT sqlite_version(*) (0.1ms) begin transaction ProjectManager Create (0.5ms) INSERT INTO "project_managers" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "Taro Yamada"], ["created_at", "2020-10-15 00:09:22.621512"], ["updated_at", "2020-10-15 00:09:22.621512"]] (1.7ms) commit transaction
pmに所属するプロジェクトをpm.projectsのように(アソシエーションメソッドと呼ぶ)関係モデルを参照し、新しいプロジェクトインスタンスpjを作成する。
irb(main):002:0> pj = pm.projects.create(name: 'Ruby Promotion PJ') (0.1ms) begin transaction Project Create (0.5ms) INSERT INTO "projects" ("name", "project_manager_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Ruby Promotion PJ"], ["project_manager_id", 1], ["created_at", "2020-10-15 00:11:50.282743"], ["updated_at", "2020-10-15 00:11:50.282743"]] (1.2ms) commit transaction
次に、プロジェクト参加メンバーを紐づけるためpj.membersのようにpjの子モデルMemberをmembersメソッドで参照し登録する。
irb(main):003:0> pj.members.create(name: 'Ichiro Suzuki') (0.1ms) begin transaction Member Create (0.5ms) INSERT INTO "members" ("name", "project_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Ichiro Suzuki"], ["project_id", 1], ["created_at", "2020-10-15 00:13:02.033894"], ["updated_at", "2020-10-15 00:13:02.033894"]] (1.2ms) commit transaction => #<Member id: 1, name: "Ichiro Suzuki", project_id: 1, created_at: "2020-10-15 00:13:02", updated_at: "2020-10-15 00:13:02">
この結果、プロジェクトマネージャーTaro Yamadaに紐づくプロジェクトRuby Promotion PJが登録され、それに紐づくメンバーIchiro Suzukiが登録された。pj.membersを呼び出すと、次のようにIchiro Suzukiを含むメンバーインスタンス配列が表示される。
irb(main):004:0> pj.members Member Load (0.4ms) SELECT "members".* FROM "members" WHERE "members"."project_id" = ? LIMIT ? [["project_id", 1], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy [#<Member id: 1, name: "Ichiro Suzuki", project_id: 1, created_at: "2020-10-15 00:13:02", updated_at: "2020-10-15 00:13:02">]>
別のメンバーKeiko Saitoをこの配列に追加してみる。 次のようにメンバーのインスタンス化を行い<<メソッドを使用して、生成されたインスタンスksをpj.membersの配列に追加する。
irb(main):006:0> ks = Member.new(name: 'Keiko Saito') irb(main):007:0> pj.members << ks (0.1ms) begin transaction Member Create (0.6ms) INSERT INTO "members" ("name", "project_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Keiko Saito"], ["project_id", 1], ["created_at", "2020-10-15 00:28:06.515357"], ["updated_at", "2020-10-15 00:28:06.515357"]] (1.9ms) commit transaction Member Load (0.3ms) SELECT "members".* FROM "members" WHERE "members"."project_id" = ? LIMIT ? [["project_id", 1], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy [#<Member id: 1, name: "Ichiro Suzuki", project_id: 1, created_at: "2020-10-15 00:13:02", updated_at: "2020-10-15 00:13:02">, #<Member id: 2, name: "Keiko Saito", project_id: 1, created_at: "2020-10-15 00:28:06", updated_at: "2020-10-15 00:28:06">]>
そしてksをsaveメソッドで保存すると、データベースのmembersテーブルにも反映される。
irb(main):008:0> ks.save => true irb(main):009:0> pj.members Member Load (0.2ms) SELECT "members".* FROM "members" WHERE "members"."project_id" = ? LIMIT ? [["project_id", 1], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy [#<Member id: 1, name: "Ichiro Suzuki", project_id: 1, created_at: "2020-10-15 00:13:02", updated_at: "2020-10-15 00:13:02">, #<Member id: 2, name: "Keiko Saito", project_id: 1, created_at: "2020-10-15 00:28:06", updated_at: "2020-10-15 00:28:06">]>
子のインスタンスから親のインスタンスを確認する方法として、メンバーIchiro Suzukiからプロジェクトマネージャー名を調べる例をみる。
Ichiro Suzukiをインスタンス化し、そのプロジェクト名とプロジェクトマネージャー名を次のように確認する。
irb(main):010:0> mem = Member.find_by(name: 'Ichiro Suzuki') Member Load (0.3ms) SELECT "members".* FROM "members" WHERE "members"."name" = ? LIMIT ? [["name", "Ichiro Suzuki"], ["LIMIT", 1]] irb(main):011:0> proj = mem.project Project Load (0.2ms) SELECT "projects".* FROM "projects" WHERE "projects"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] irb(main):012:0> proj.name => "Ruby Promotion PJ" irb(main):013:0> proj.project_manager.name ProjectManager Load (0.2ms) SELECT "project_managers".* FROM "project_managers" WHERE "project_managers"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] => "Taro Yamada"
アソシエーションメソッドを使用しなくても、findメソッドやwhereメソッドを使用することで同じように情報を得ることができるが、Railsでは、アソシエーションメソッドをうまく使用して処理をシンプルに記述することが推奨されている。
多対多
多対多の関係を実装する方法はhmt型とhabtm型の2種類の方法がある。
hmt型
has_manyメソッドのthroughオプションを使用する方法。2つのモデルを双方の子モデルを通して関係付ける。
図書館の図書とユーザの関係モデルを考えてみる。一般的には、2つのモデルの関係が直接多対多になることはなく、2つのモデルの間には必ず双方に関連する子モデルが存在する。
モデルの関係図↓
LibraryアプリケーションにRentalモデルとして貸し出し機能を追加していく。Rentalモデルを生成する。 (貸し出し処理はコントローラーとして実装することになるが、ここでは貸し出し機能のためのRentalモデルについてだけ考える)
% bin/rails g model rental user:references book:references rental_date:date Running via Spring preloader in process 2701 invoke active_record create db/migrate/20201015015339_create_rentals.rb create app/models/rental.rb invoke test_unit create test/models/rental_test.rb create test/fixtures/rentals.yml
生成されたRentalモデルは次のようになる。
# app/models/rental.rb class Rental < ApplicationRecord belongs_to :user belongs_to :book end
そしてUserモデルとBookモデルにthroughオプションを指定して1対多の関係を設定する。
# app/models/user.rb class User < ApplicationRecord has_many :rentals has_many :books, through: :rentals end
# app/models/book.rb class Book < ApplicationRecord has_many :rentals has_many :users, through: :rentals ...(省略) end
マイグレーションし生成されたファイルは以下のようになっている。
# db/migrate/20201015015339_create_rentals.rb class CreateRentals < ActiveRecord::Migration[6.0] def change create_table :rentals do |t| t.references :user, null: false, foreign_key: true t.references :book, null: false, foreign_key: true t.date :rental_date t.timestamps end end end
references型として設定したuserとbookは外部キーであり、そのためのオプションとしてforeign_key: true
が付加されている。
Railsコンソールを使ってアソシエーション機能を確認してみる。 まずはuserを作成する。
irb(main):001:0> user = User.create(name: '山田花子', email: 'hanako@yahoo.co.jp') (0.5ms) SELECT sqlite_version(*) (0.1ms) begin transaction User Create (0.6ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "山田花子"], ["email", "hanako@yahoo.co.jp"], ["created_at", "2020-10-15 02:13:37.672782"], ["updated_at", "2020-10-15 02:13:37.672782"]] (1.1ms) commit transaction
user.booksを呼び出すと、ユーザからみた貸し出し図書のインスタンス配列ActiveRecord::Associations::CollectionProxy []が作成される。空であるためまだ1件も貸し出し図書を持っていないことがわかる。
irb(main):002:0> user.books Book Load (0.2ms) SELECT "books".* FROM "books" INNER JOIN "rentals" ON "books"."id" = "rentals"."book_id" WHERE "rentals"."user_id" = ? LIMIT ? [["user_id", 1], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy []>
新しい図書インスタンスbookを作成し、user.booksに追加することで貸し出し図書を持たせる。
irb(main):003:0> book = Book.create(title: 'Railsの夜明け') (0.1ms) begin transaction Book Create (0.5ms) INSERT INTO "books" ("title", "created_at", "updated_at") VALUES (?, ?, ?) [["title", "Railsの夜明け"], ["created_at", "2020-10-15 02:14:31.562011"], ["updated_at", "2020-10-15 02:14:31.562011"]] (1.2ms) commit transaction irb(main):004:0> user.books << book (0.1ms) begin transaction Rental Create (0.5ms) INSERT INTO "rentals" ("user_id", "book_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["user_id", 1], ["book_id", 6], ["created_at", "2020-10-15 02:15:05.114613"], ["updated_at", "2020-10-15 02:15:05.114613"]] (1.2ms) commit transaction Book Load (0.2ms) SELECT "books".* FROM "books" INNER JOIN "rentals" ON "books"."id" = "rentals"."book_id" WHERE "rentals"."user_id" = ? LIMIT ? [["user_id", 1], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy [#<Book id: 6, title: "Railsの夜明け", description: nil, created_at: "2020-10-15 02:14:31", updated_at: "2020-10-15 02:14:31">]>
他の新しい本を借りるとして、同様に関係付けを行う。
irb(main):005:0> book = Book.create(title: 'Rubyの魔法') (0.1ms) begin transaction Book Create (0.5ms) INSERT INTO "books" ("title", "created_at", "updated_at") VALUES (?, ?, ?) [["title", "Rubyの魔法"], ["created_at", "2020-10-15 02:16:01.915280"], ["updated_at", "2020-10-15 02:16:01.915280"]] (1.8ms) commit transaction irb(main):006:0> user.books << book (0.1ms) begin transaction Rental Create (0.7ms) INSERT INTO "rentals" ("user_id", "book_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["user_id", 1], ["book_id", 7], ["created_at", "2020-10-15 02:16:21.719510"], ["updated_at", "2020-10-15 02:16:21.719510"]] (1.8ms) commit transaction Book Load (0.3ms) SELECT "books".* FROM "books" INNER JOIN "rentals" ON "books"."id" = "rentals"."book_id" WHERE "rentals"."user_id" = ? LIMIT ? [["user_id", 1], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy [#<Book id: 6, title: "Railsの夜明け", description: nil, created_at: "2020-10-15 02:14:31", updated_at: "2020-10-15 02:14:31">, #<Book id: 7, title: "Rubyの魔法", description: nil, created_at: "2020-10-15 02:16:01", updated_at: "2020-10-15 02:16:01">]>
この結果、ユーザ山田花子が借りている(関連付けられている)図書の一覧を取得する。
irb(main):007:0> hanako = User.find_by(name: '山田花子') User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "山田花子"], ["LIMIT", 1]] irb(main):008:0> hanako.books.pluck(:title) (0.3ms) SELECT "books"."title" FROM "books" INNER JOIN "rentals" ON "books"."id" = "rentals"."book_id" WHERE "rentals"."user_id" = ? [["user_id", 1]] => ["Railsの夜明け", "Rubyの魔法"]
まずはインスタンスhanakoを生成し、アソシエーションメソッドを使用してhanako.booksで貸し出し図書の情報を取得している。タイトル名だけを取り出すため、pluckメソッドでタイトルを指定している。
図書側から借主を参照することもできる。
irb(main):009:0> book = Book.find_by(title: 'Rubyの魔法') Book Load (0.2ms) SELECT "books".* FROM "books" WHERE "books"."title" = ? LIMIT ? [["title", "Rubyの魔法"], ["LIMIT", 1]] irb(main):010:0> book.users.pluck(:name) (0.3ms) SELECT "users"."name" FROM "users" INNER JOIN "rentals" ON "users"."id" = "rentals"."user_id" WHERE "rentals"."book_id" = ? [["book_id", 7]] => ["山田花子"]
habtm型
2つめはhas_and_belongs_to_manyメソッドを使う方法で、具体的には、意味のある子モデルを介した結合ではなく、仲介のためのテーブル(中間テーブル)を作成し関連付けを行う。
中間テーブルとは、対応するモデルを持たないテーブルであり、結合する双方のモデルで外部キーとして使うidのみを持つテーブルである。中間テーブルのテーブル名は、結合する2つのモデル名の複数形をアルファベット順に_で区切って並べたものになる。
ユーザと図書モデルを考える。まずは相互のモデルを参照するためにhas_and_belongs_to_manyメソッドを設定する。
# app/models/user.rb class User < ApplicationRecord has_and_belongs_to_many :books end
# app/models/book.rb class Book < ApplicationRecord has_and_belongs_to_many :users ...(省略) end
子モデルを必要としないため、仲介テーブルを作成するためのマイグレーションファイルのみを作成する。
% bin/rails g migration create_books_users book:references user:references
この結果、次のマイグレーションファイルが生成される。
class CreateBooksUsers < ActiveRecord::Migration[6.0] def change create_table :books_users do |t| t.references :book, foreign_key: true t.references :user, foreign_key: true end end end
一般的には、モデルとモデルが対等な関係を持つ場合、双方向の手続きに相当するオブジェクトが存在すると考えるのが自然である。その手続きに関する属性を持つ1つのモデルとして考えることができるため、通常多対多の関係はhmt型で構築するのが自然であると考えられる。