web-dev-qa-db-ja.com

accepts_nested_attributes_forとfind_or_create?

Railsのaccepts_nested_attributes_forメソッドを使用して成功していますが、どのようにすればnotレコードが既に存在する場合に新しいレコードを作成できますか?

例として:

たとえば、チーム、メンバーシップ、プレーヤーの3つのモデルがあり、各チームにはメンバーシップを通じて多くのプレーヤーがいます。プレーヤーは多くのチームに所属できます。チームモデルはプレーヤーのネストされた属性を受け入れる可能性がありますが、これは、チームとプレーヤーの組み合わせフォームを通じて送信された各プレーヤーが新しいプレーヤーレコードとして作成されることを意味します。

同じ名前のプレーヤーがまだいない場合にのみ、この方法で新しいプレーヤーレコードを作成するにはどうすればよいですか? is同じ名前のプレーヤーがいる場合、新しいプレーヤーレコードは作成されませんが、正しいプレーヤーが見つかり、新しいチームレコードに関連付けられます。

55
trisignia

自動保存の関連付けのフックを定義すると、通常のコードパスがスキップされ、代わりにメソッドが呼び出されます。したがって、これを行うことができます:

class Post < ActiveRecord::Base
  belongs_to :author, :autosave => true
  accepts_nested_attributes_for :author

  # If you need to validate the associated record, you can add a method like this:
  #     validate_associated_record_for_author
  def autosave_associated_records_for_author
    # Find or create the author by name
    if new_author = Author.find_by_name(author.name)
      self.author = new_author
    else
      self.author.save!
    end
  end
end

このコードはテストされていませんが、ほとんど必要なコードです。

54

プレーヤーをチームに追加することではなく、メンバーシップをチームに追加することと考えてください。フォームはプレーヤーと直接連動しません。メンバーシップモデルにはplayer_name仮想属性。舞台裏では、プレーヤーを検索したり、プレーヤーを作成したりできます。

class Membership < ActiveRecord::Base
  def player_name
    player && player.name
  end

  def player_name=(name)
    self.player = Player.find_or_create_by_name(name) unless name.blank?
  end
end

次に、player_nameテキストフィールドをメンバーシップフォームビルダーに追加します。

<%= f.text_field :player_name %>

このように、accepts_nested_attributes_forに固有のものではなく、任意のメンバーシップフォームで使用できます。

注:この手法では、検証が行われる前にPlayerモデルが作成されます。このエフェクトが必要ない場合は、プレーヤーをインスタンス変数に格納して、before_saveコールバックに保存します。

30
ryanb

:accepts_nested_attributes_forを使用する場合、既存のレコードのidを送信すると、新しいレコードが作成される代わりに、ActiveRecordがpdate既存のレコードになります。あなたのマークアップがどのようなものかはわかりませんが、おおよそ次のようなものを試してください:

<%= text_field_tag "team[player][name]", current_player.name %>
<%= hidden_field_tag "team[player][id]", current_player.id if current_player %>

idが指定されている場合、プレーヤー名は更新されますが、それ以外の場合は作成されます。

autosave_associated_record_for_メソッドを定義するアプローチは非常に興味深いものです。必ず使います!ただし、この簡単なソリューションも検討してください。

4
Anson

これは、has_oneまたはbelongs_toの関係がある場合にうまく機能します。しかし、has_manyまたはhas_many throughで不十分でした。

Has_many:through関係を利用するタグ付けシステムがあります。ここでの解決策のどちらも、私が行く必要がある場所に行きませんでしたので、私は他の人を助けるかもしれない解決策を思いつきました。これはRails 3.2でテストされています。

セットアップ

ここに私のモデルの基本的なバージョンがあります:

場所オブジェクト:

class Location < ActiveRecord::Base
    has_many :city_taggables, :as => :city_taggable, :dependent => :destroy
    has_many :city_tags, :through => :city_taggables

    accepts_nested_attributes_for :city_tags, :reject_if => :all_blank, allow_destroy: true
end

オブジェクトにタグを付ける

class CityTaggable < ActiveRecord::Base
   belongs_to :city_tag
   belongs_to :city_taggable, :polymorphic => true
end

class CityTag < ActiveRecord::Base
   has_many :city_taggables, :dependent => :destroy
   has_many :ads, :through => :city_taggables
end

解決

実際、autosave_associated_recored_forメソッドを次のようにオーバーライドしました。

class Location < ActiveRecord::Base
   private

   def autosave_associated_records_for_city_tags
     tags =[]
     #For Each Tag
     city_tags.each do |tag|
       #Destroy Tag if set to _destroy
       if tag._destroy
         #remove tag from object don't destroy the tag
         self.city_tags.delete(tag)
         next
       end

       #Check if the tag we are saving is new (no ID passed)
       if tag.new_record?
         #Find existing tag or use new tag if not found
         tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
       else
         #If tag being saved has an ID then it exists we want to see if the label has changed
         #We find the record and compare explicitly, this saves us when we are removing tags.
         existing = CityTag.find_by_id(tag.id)
         if existing    
           #Tag labels are different so we want to find or create a new tag (rather than updating the exiting tag label)
           if tag.label != existing.label
             self.city_tags.delete(tag)
             tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
           end
         else
           #Looks like we are removing the tag and need to delete it from this object
           self.city_tags.delete(tag)
           next
         end
       end
       tags << tag
     end
     #Iterate through tags and add to my Location unless they are already associated.
     tags.each do |tag|
       unless tag.in? self.city_tags
         self.city_tags << tag
       end
     end
   end

上記の実装は、ネストされたフォームでfields_forを使用するときに必要な方法でタグを保存、削除、変更します。簡略化する方法があれば、フィードバックをお待ちしています。タグラベルを更新するのではなく、ラベルが変更されたときに明示的にタグを変更していることを指摘することが重要です。

3
Dustin M.

before_validationフックは適切な選択です。これは標準のメカニズムであり、あいまいなautosave_associated_records_for_*をオーバーライドするよりもコードが単純になります。

class Quux < ActiveRecord::Base

  has_and_belongs_to_many :foos
  accepts_nested_attributes_for :foos, reject_if: ->(object){ object[:value].blank? }
  before_validation :find_foos

  def find_foos
    self.foos = self.foos.map do |object|
      Foo.where(value: object.value).first_or_initialize
    end
  end

end
3
vemv

質問(find_or_createを参照)の観点からすべてを整理するために、Francoisの回答のifブロックは次のように言い換えることができます。

self.author = Author.find_or_create_by_name(author.name) unless author.name.blank?
self.author.save! 
3
KenB

@FrançoisBeausoleilの回答は素晴らしく、大きな問題を解決しました。 autosave_associated_record_forの概念について学ぶのは素晴らしいことです。

ただし、この実装では1つのコーナーケースが見つかりました。既存の投稿の著者(A1)のupdateの場合、新しい著者名(A2)が渡されると、元の(A1)の著者名が変更されます。

p = Post.first
p.author #<Author id: 1, name: 'JK Rowling'>
# now edit is triggered, and new author(non existing) is passed(e.g: Cal Newport).

p.author #<Author id: 1, name: 'Cal Newport'>

元のコード:

class Post < ActiveRecord::Base
  belongs_to :author, :autosave => true
  accepts_nested_attributes_for :author

  # If you need to validate the associated record, you can add a method like this:
  #     validate_associated_record_for_author
  def autosave_associated_records_for_author
    # Find or create the author by name
    if new_author = Author.find_by_name(author.name)
      self.author = new_author
    else
      self.author.save!
    end
  end
end

編集の場合、投稿のself.authorは既にid:1の作成者であり、他の場所に移動してブロックし、新しい変数を作成する代わりにそのauthorを更新するためです。

この問題を軽減するためにコード(elsif条件)を変更しました:

class Post < ActiveRecord::Base
  belongs_to :author, :autosave => true
  accepts_nested_attributes_for :author

  # If you need to validate the associated record, you can add a method like this:
  #     validate_associated_record_for_author
  def autosave_associated_records_for_author
    # Find or create the author by name
    if new_author = Author.find_by_name(author.name)
      self.author = new_author
    elsif author && author.persisted? && author.changed?
      # New condition: if author is already allocated to post, but is changed, create a new author.
      self.author = Author.new(name: author.name)
    else
      # else create a new author
      self.author.save!
    end
  end
end
1
kiddorails

@ dustin-mの答えは私にとって有益でした-私はhas_many:through関係で何かカスタムを行っています。私には、1つのトレンドを持つトピックがあり、多くの子供(再帰的)があります。

これを標準の_has_many :searches, through: trend, source: :children_関係として構成すると、ActiveRecordは気に入らなくなります。 topic.trendとtopic.searchesを取得しますが、topic.searches.create(name:foo)は実行しません。

したがって、上記を使用してカスタム自動保存を構築し、_accepts_nested_attributes_for :searches, allow_destroy: true_ def autosave_associated_records_for_searches searches.each do | s | if s._destroy self.trend.children.delete(s) elsif s.new_record? self.trend.children << s else s.save end end endで正しい結果を達成しています

0
David Hersey