LoginSignup
23
9

More than 5 years have passed since last update.

has_many through の新規レコードを保存する時に気をつけること

Posted at

Rails というか ActiveRecord で has_many through なモデルをさわっていてハマった事象があったので書きます。ただ、ハマりを回避する方法は分かっても、なぜそのような挙動になるかはまだ分かっていません。誰か教えてもらえると助かります。

前提

  • Rails 5.x
  • ActiveRecord

モデル

class User < ApplicationRecord
  has_many :articles
  has_many :game_users
  has_many :games, through: :game_users
  belongs_to :pinned_article, class_name: 'Article'
end

class Article < ApplicationRecord
  belongs_to :user
end

class Game < ApplicationRecord
  has_many :game_users
  has_many :users, through: :game_users
end

class GameUser < ApplicationRecord
  belongs_to :game
  belongs_to :user
end

ER.png

User と Game が has_many through の関係になっています。そして、 User と Article が has_many になっています。また、 User.pinned_article で Article を belongs_to で参照しています。

親子関係をまとめて save

User を基点として親子関係をまとめて save しようと思い、以下のようなコードを書きました。

# Game は予め作っておく
Game.create

u = User.new
a = u.articles.build
u.pinned_article = a
u.games << Game.last
u.save!

これを実行すると以下のような SQL が発行されます。

BEGIN

INSERT INTO `users` (`created_at`, `updated_at`) VALUES ('2017-12-15 02:24:44', '2017-12-15 02:24:44')
INSERT INTO `articles` (`user_id`, `created_at`, `updated_at`) VALUES (35, '2017-12-15 02:24:44', '2017-12-15 02:24:44')
INSERT INTO `game_users` (`game_id`, `user_id`, `created_at`, `updated_at`) VALUES (9, 35, '2017-12-15 02:24:44', '2017-12-15 02:24:44')
UPDATE `users` SET `pinned_article_id` = 32 WHERE `users`.`id` = 35
INSERT INTO `game_users` (`game_id`, `user_id`, `created_at`, `updated_at`) VALUES (9, 35, '2017-12-15 02:24:44', '2017-12-15 02:24:44')

COMMIT

一見上手く行っているように見えますが、最後の SQL でなぜか game_users に同じレコードが重複して INSERT されています。User.pinned_article の ID を反映するために、 INSERT 後に改めて UPDATE が走るのですが、なぜか関係のない GameUser の中間テーブルデータが改めて INSERT されています。

実際には、中間テーブルを作った場合重複データを防ぐために DB や Rails でユニーク制約を付けると思うので、SQL が失敗して ROLLBACK してしまいます。

class GameUser < ApplicationRecord
  belongs_to :game
  belongs_to :user

  validates :user, uniqueness: { scope: [:game_id] }
end

期待した動きをするコード

以下のコードにすると期待したとおりの動きをします。違いは User.games に直接 append するのではなく、中間テーブルの GameUser を build して作成する所です。

u = User.new
a = u.articles.build
u.pinned_article = a

# build をして作る
gu = u.game_users.build
gu.game = Game.last

u.save!

これで無事できました。基本は build 系を使っていくのが良いんですかね。

<<

ちなみに ActiveRecord の << は collection_proxy で定義されています。

def <<(*records)
  proxy_association.concat(records) && self
end
alias_method :push, :<<
alias_method :append, :<<

この append を実行すると、親の ID が確定していたら即時 INSERT が実行されます。 save とか必要ありません。

なぜこうなるのか

ActiveRecord の動きを step 実行しながら追っていきました。が、結論、なぜそうなるのかは時間切れで解明できず…。無念。また時間を作って調査をしたいと思います。

save! で保存をする時に save_collection_association が呼ばれるのですが、ここでなぜか最後に重複データが渡ってきてしまうという…。

23
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
9