has_many through의 새로운 레코드를 저장할 때 조심하십시오.

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
    



    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, :<<
    
  • htps : // 기주 b. 코 m / 라이 ls / 라이 ls / b / b / 33 934697370892 치온_p로 xy. rb#L1054-L1058

  • append를 실행하면 부모 ID가 확인되면 즉시 INSERT가 실행됩니다. save 혹은 필요 없습니다.

    왜 이렇게 되는가



    ActiveRecord의 움직임을 step 실행하면서 쫓아갔습니다. 하지만, 결론, 왜 그렇게 되는지는 시간 만에 해명할 수 없어… 무념. 또 시간을 만들어 조사를 하고 싶습니다.
    save! 로 저장할 때 save_collection_association 가 불립니다만, 여기서 왠지 마지막에 중복 데이터가 건너 와 버린다고 한다.

    좋은 웹페이지 즐겨찾기