好きなものだけ書く。ポジティブに。

好きなことを楽しく。プログラミング、写真、音楽、ガジェットとか。

Rails で多対多のリレーションをした時に、joinレコードも同時にnew & save したい

おつかれさまです!のぶじゃすです。

最近よくRails7でコードを書いています。modelsを作っていく中でどうしても多対多のモデルになることがあります。

多対多のリレーションをするときには中間モデル(join モデル)を作る必要があるのですが、create時に中間モデルも一緒にcreateする必要があります。自動でcreateされると思っていたのですが、されなくて困りました。色々試して解決できた&あまり明確に書いてある記事が見当たらなかったので備忘録的にBLOG書いておきます。

サーバの線につまづいて転びそうになる人のイラスト
モデル周りはよく躓きますよね

今回使用したバージョン

  • Ruby 3.0.3
  • Rails 7.0.0

うさぎと親が多対多

今回私が作ったのはこんな感じのモデルです。簡単に説明すると、1人で複数のうさぎを飼育できるし、1匹のうさぎを複数人で飼育することも可能です。

まあそうですよね。1匹飼ったらもう1匹飼いたくなるのが人情ですよね。

models/rabbit.rb
class Rabbit < ApplicationRecord
  has_many :breeds, dependent: :destroy
  has_many :users, through: :breeds
end
models/breed.rb
class Breed < ApplicationRecord
  belongs_to :rabbit
  belongs_to :user
end
models/user.rb
class User < ApplicationRecord
  has_many :breeds, dependent: :destroy
  has_many :rabbits, through: :breeds
end

has_many :through関連付け

今回は has_and_belongs_to_many ではなくて、 has_many :through の方法でリレーションシップしています。

詳細はRailsガイドをどうぞ

元々のうさぎ新規作成機能

controllers/rabbits_controller.rb#create
# POST /rabbits or /rabbits.json
def create
  @rabbit = Rabbit.new(rabbit_params)
  respond_to do |format|
    if @rabbit.save
      format.html { redirect_to rabbit_url(@rabbit), notice: 'Rabbit was successfully created.' }
      format.json { render :show, status: :created, location: @rabbit }
    else
      format.html { render :new, status: :unprocessable_entity }
      format.json { render json: @rabbit.errors, status: :unprocessable_entity }
    end
  end
end

scaffoldのままなので説明はあまりいらないですかね。 new して save する。それだけです。

間違った方法

私はこれでうまくいくと思っていました。しかしこれだと中間オブジェクトが自動で保存されず、うまくいきません。

irb(main):005:0> rabbit = current_user.rabbits.new name: 'test', birthday: '2021-01-01'
=> #<Rabbit:0x00007f1f3cb30120 id: nil, name: "test", birthday: Fri, 01 Jan 2021>
irb(main):006:0> rabbit.save
  TRANSACTION (1.1ms)  BEGIN
  Rabbit Create (0.7ms)  INSERT INTO "rabbits" ("name", "birthday") VALUES ($1, $2) RETURNING "id"  [["name", "test"], ["birthday", "2021-01-01"]]
  TRANSACTION (1.4ms)  COMMIT
=> true

うさぎしか作成されないので、 current_user.rabbits で参照できません。

正しい方法

rabbit.save ではなく current_user.save をすると中間オブジェクトも一緒に保存されます。うまくいきました。

irb(main):008:0> rabbit = current_user.rabbits.new name: 'test', birthday: '2021-01-01'
=> #<Rabbit:0x00007f1f3d06cd28 id: nil, name: "test", birthday: Fri, 01 Jan 2021>
irb(main):009:0> current_user.save
  TRANSACTION (0.9ms)  BEGIN
  Rabbit Create (0.9ms)  INSERT INTO "rabbits" ("name", "birthday") VALUES ($1, $2) RETURNING "id"  [["name", "test"], ["birthday", "2021-01-01"]]
  Breed Create (0.9ms)  INSERT INTO "breeds" ("rabbit_id", "user_id") VALUES ($1, $2) RETURNING "id"  [["rabbit_id", 11], ["user_id", 2]]
  TRANSACTION (1.4ms)  COMMIT
=> true

これで正しくリレーションされるので current_user.rabbits で参照できるようになります。

最終的な controller は current_user.save にしました

controllers/rabbits_controller.rb#create

ここまで読んでくれた貴方はもうわかっていると思いますが、結論はこんな感じになりました

# POST /rabbits or /rabbits.json
def create
  @rabbit = current_user.rabbits.new(rabbit_params)
  respond_to do |format|
    if current_user.save
      format.html { redirect_to rabbit_url(@rabbit), notice: 'Rabbit was successfully created.' }
      format.json { render :show, status: :created, location: @rabbit }
    else
      format.html { render :new, status: :unprocessable_entity }
      format.json { render json: @rabbit.errors, status: :unprocessable_entity }
    end
  end
end

しかしまだ、なぜこうなるのかちゃんと説明できてないので、ActiveRecordのコードを読んでおこうと思います。

アクセスありがとうございます🙇‍♂️

ここまで読んでいただき誠にありがとうございました。もしこの記事が役に立ったらはてブや、Twitterのフォローしていただけると大変喜びます😊