見出し画像

Rails アソシエーション

Railsの関連付け(アソシエーション)は、2つのモデル同士のつながりを指しています。
関連づけには以下のようなものがあります。

  • belongs_to

  • has_one

  • has_many

  • has_many :through

  • has_one :through

  • has_and_belongs_to_many

今回は主に使うことの多い belongs_tohas_many を使ってアソシエーションを見ていきます。
この記事では事前に作成したコードを使用していきます。そのため、以下のマガジンに記載されている内容を行っている前提の記事となっています。userbook というモデルを使います。それぞれのモデルの作成は以下のマガジンを見ていただくと準備することができます。

belongs_to

まずは belongs_to 関連付けを行っていきます。これを行うことで他方のモデルとの間に「1対1」のつながりが設定されます。つまり、bookuser との間に「1対1」のつながりが設定されます。
関連付けを行うためにはまず books テーブルに users テーブルの id を追加します。
以下のコマンドを実行します。

bundle e rails g migration AddUserRefToBooks user:references

以下のようなマイグレーションファイルが作成されます。

class AddUserRefToBooks < ActiveRecord::Migration[7.0]
  def change
    add_reference :books, :user, null: false, foreign_key: true
  end
end

マイグレーションファイルを作っただけではデータベースにテーブルあるいはカラムは作成されません。そのため、以下のコマンドを実行します。

bundle e rails db:migrate:reset

schema.rb を確認します。books テーブルに user_id カラムが追加されていれば成功です。
seeds.rb を修正します。user 1つあたり、100件の book のデータを持つように修正します。

book_attributes = User.all.pluck(:id).flat_map do |user_id|
  book_attributes.map { |book_attribute| book_attribute.merge(user_id:) }
end
book_attributes.each do |book_attribute|
  Book.find_or_create_by(title: book_attribute[:title], user_id: book_attribute[:user_id])
end

データが作成されていることを確認します。rails c を実行します。

bundle e rails c

以下のコードを実行してデータが作成されていることを確認します。

irb(main):001:0> User.count
  User Count (1.4ms)  SELECT COUNT(*) FROM "users"
=> 50
irb(main):002:0> Book.count
  Book Count (4.6ms)  SELECT COUNT(*) FROM "books"
=> 5000

ついでに、アソシエーションをたどってbook に紐づいた user が取得できる確認します。

irb(main):003:0> Book.first.user
  Book Load (1.1ms)  SELECT "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT ?  [["LIMIT", 1]]
/usr/local/bundle/gems/activemodel-7.0.4.1/lib/active_model/attribute_methods.rb:458:in `method_missing': undefined method `user' for #<Book id: 1, title: "test1", created_at: "2023-01-27 03:14:10.000901000 +0000", updated_at: "2023-01-27 03:14:10.000901000 +0000", user_id: 1> (NoMethodError)
Did you mean?  user_id

まだこの段階ではアソシエーションをたどってbook に紐づいた user が取得できません。
これを実現するために app/models/book.rb belongs_to を追加します。

belongs_to :user

そして rails c でアソシエーションをたどってbook に紐づいた user が取得できるか確認します。

irb(main):001:0> Book.first.user
  Book Load (0.8ms)  SELECT "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT ?  [["LIMIT", 1]]
  User Load (1.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, email: "test1@example.com", created_at: "2023-01-27 03:07:57.850304000 +0000", updated_at: "2023-01-27 03:07:57.850304000 +0000", first_name: "test1", last_name: "test1">

belongs_to を追加したことによって、アソシエーションをたどって book に紐づいた user が取得できるようになりました。これで belongs_to のアソシエーションは完了です。

has_many

次に has_many 関連付けを行っていきます。これを行うことで他方のモデルとの間に「1対多」のつながりが設定されます。つまり、userbook の間に「1対多」のつながりが設定されます。そのため、1つの user  book をたくさん持てるようになります。

現状の app/models/user.rb には has_many のコードが実装されていません。その状態で user に紐づく books を取得しようとするとどうなるか確認します。

bundle e rails c
...
irb(main):001:0> User.first.books
  User Load (0.9ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
/usr/local/bundle/gems/activemodel-7.0.4.1/lib/active_model/attribute_methods.rb:458:in `method_missing': undefined method `books' for #<User id: 1, email: "test1@example.com", created_at: "2023-01-27 03:07:57.850304000 +0000", updated_at: "2023-01-27 03:07:57.850304000 +0000", first_name: "test1", last_name: "test1"> (NoMethodError)

エラーが出てしまい、取得することができません。
そこで、app/models/user.rb has_manyを追加します。

has_many :books, dependent: :destroy

修正を行なったので rails c を実行して user に紐づく books を取得できるか確認します。

irb(main):001:0> User.first.books
  User Load (1.0ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Book Load (0.8ms)  SELECT "books".* FROM "books" WHERE "books"."user_id" = ?  [["user_id", 1]]
=>
[#<Book:0x0000ffff887a35e0 id: 1, title: "test1", created_at: Fri, 27 Jan 2023 03:14:10.000901000 UTC +00:00, updated_at: Fri, 27 Jan 2023 03:14:10.000901000 UTC +00:00, user_id: 1>,
 #<Book:0x0000ffff887b9660 id: 2, title: "test2", created_at: Fri, 27 Jan 2023 03:14:10.015271000 UTC +00:00, updated_at: Fri, 27 Jan 2023 03:14:10.015271000 UTC +00:00, user_id: 1>,
 #<Book:0x0000ffff887b9480 id: 3, title: "test3", created_at: Fri, 27 Jan 2023 03:14:10.027413000 UTC +00:00, updated_at: Fri, 27 Jan 2023 03:14:10.027413000 UTC +00:00, user_id: 1>,
 #<Book:0x0000ffff887b92f0 id: 4, title: "test4", created_at: Fri, 27 Jan 2023 03:14:10.038574000 UTC +00:00, updated_at: Fri, 27 Jan 2023 03:14:10.038574000 UTC +00:00, user_id: 1>,
 #<Book:0x0000ffff887b9160 id: 5, title: "test5", created_at: Fri, 27 Jan 2023 03:14:10.049142000 UTC +00:00, updated_at: Fri, 27 Jan 2023 03:14:10.049142000 UTC +00:00, user_id: 1>,
 #<Book:0x0000ffff887b8fd0 id: 6, title: "test6", created_at: Fri, 27 Jan 2023 03:14:10.062240000 UTC +00:00, updated_at: Fri, 27 Jan 2023 03:14:10.062240000 UTC +00:00, user_id: 1>,
 #<Book:0x0000ffff887b8e40 id: 7, title: "test7", created_at: Fri, 27 Jan 2023 03:14:10.074248000 UTC +00:00, updated_at: Fri, 27 Jan 2023 03:14:10.074248000 UTC +00:00, user_id: 1>,
 #<Book:0x0000ffff887b8cb0 id: 8, title: "test8", created_at: Fri, 27 Jan 2023 03:14:10.086000000 UTC +00:00, updated_at: Fri, 27 Jan 2023 03:14:10.086000000 UTC +00:00, user_id: 1>,
 #<Book:0x0000ffff887b8b20 id: 9, title: "test9", created_at: Fri, 27 Jan 2023 03:14:10.097502000 UTC +00:00, updated_at: Fri, 27 Jan 2023 03:14:10.097502000 UTC +00:00, user_id: 1>,
 #<Book:0x0000ffff887b8990 id: 10, title: "test10", created_at: Fri, 27 Jan 2023 03:14:10.107716000 UTC +00:00, updated_at: Fri, 27 Jan 2023 03:14:10.107716000 UTC +00:00, user_id: 1>,
...

取得することができました。seeds.rb ファイルに定義した通り、1つの user あたり 100件の books のデータが紐づいているはずです。
has_many を追加したことによって、アソシエーションをたどって user に紐づいた books が取得できるようになりました。これで has_many のアソシエーションは完了です。

既存コードの修正

アソシエーションに関する説明は完了しています。そのため、アソシエーションに関することのみを知りたかった場合はここより下の内容を読む必要はありません。
今のままでは以下のマガジンで実装した処理にエラーが出てしまいます。

試しにテストを実行してみます。

bundle e rspec

レッドになります。実装を修正したことにより、テストがレッドになってしまうようになりました。データベース上は book を作成するために、user のデータが必要になりました。しかし、既存のアプリケーションのコードはそのようなデータを作成するようになっていません。そのため、テストを修正していきます。
まずは spec/factories/books.rb を修正します。

FactoryBot.define do
  factory :book do
    association :user
    title { "MyString" }
  end
end

このように association を追加することによって book のデータに user のデータが紐づくようになります。アプリケーションのデータ構造に合わせてテストのデータ構造も修正したということです。
再度テストを実行します。

bundle e rspec

まだレッドです。具体的には spec/requests/books_spec.rb のテストが落ちてしまいます。その中でも以下のテストが落ちてしまいます。

describe "POST /books" do
  let(:params) { { book: { title: } } }
  let(:title) { 'test' }

  context '正常系' do
    it '302 を返す' do
      post "/books", params: params
      expect(response).to have_http_status(:found)
    end
  end
end

アプリケーションコードは以下のようになっています。

def create
  @book = Book.new(book_params)
  if @book.save
    redirect_to books_path
  else
    render :new
  end
end

テストが落ちてしまう原因は book のデータを作成する際に、user に紐づくようになっていないからです。そこで、アプリケーションのコードを以下のように修正してみます。

def create
  @book = current_user.books.new(book_params)
  if @book.save
    redirect_to books_path
  else
    render :new
  end
end

そして、テストを実行します。

bundle e rspec spec/requests/books_spec.rb

テストがグリーンになりました。テストをすべて実行してみます。

bundle e rspec

テストがすべてグリーンになりました。修正が正しく完了したようです。
ブラウザからデータを投稿することも確認します。
新規作成ページからデータを登録することができていると思います。
これで修正は完了です。

この記事が気に入ったらサポートをしてみませんか?