akceptuje zagnieżdżone atrybuty za pomocą Znajdź lub utwórz?

Używam metody accepts_nested_attributes_for z wielkim sukcesem, ale jak Mogę sprawić, by nie tworzyła nowe rekordy, jeśli rekord już istnieje?

Na przykład:

Powiedzmy, że mam trzy modele, drużynę, członkostwo i Gracza, a każda drużyna ma wielu graczy poprzez członkostwo, a gracze mogą należeć do wielu drużyn. Model drużynowy może wtedy akceptować zagnieżdżone atrybuty dla graczy, ale oznacza to, że każdy gracz zgłoszony przez połączoną drużynę+gracza) formularz zostanie utworzony jako nowy rekord gracza.

Jak mam robić rzeczy, jeśli chcę stworzyć nowy rekord gracza tylko w ten sposób, jeśli nie ma już gracza o tej samej nazwie? Jeśli istnieje gracz o tym samym nazwisku, nie należy tworzyć nowych rekordów graczy, ale zamiast tego należy znaleźć właściwego gracza i powiązać go z nowym rekordem drużyny.

Author: trisignia, 2010-08-27

8 answers

Gdy zdefiniujesz hook dla asocjacji automatycznego zapisu, normalna ścieżka kodu jest pomijana, a twoja metoda jest wywoływana. W ten sposób możesz to zrobić:

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

Ten kod nie jest testowany, ale powinien być tym, czego potrzebujesz.

 50
Author: François Beausoleil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-22 06:59:20

Nie myśl o tym jak o dodawaniu graczy do drużyn, myśl o tym jak o dodawaniu członków do drużyn. Forma nie działa bezpośrednio z graczami. Model członkostwa może mieć wirtualny atrybut player_name. Za kulisami może to albo wyszukać gracza lub utworzyć.

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

A następnie po prostu dodaj pole tekstowe player_name do dowolnego kreatora formularzy członkostwa.

<%= f.text_field :player_name %>

W ten sposób nie jest specyficzny dla accepts_nested_attributes_for i może być używany w dowolnej formie członkostwa.

Uwaga: Dzięki tej technice Model gracza jest tworzony przed walidacją. Jeśli nie chcesz tego efektu, przechowuj Odtwarzacz w zmiennej instancji, a następnie zapisz go w wywołaniu zwrotnym before_save.

 28
Author: ryanb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-14 15:08:38

Podczas używania :accepts_nested_attributes_for, wysłanie idistniejącego rekordu spowoduje, że ActiveRecord zaktualizuje istniejący rekord zamiast tworzyć nowy rekord. Nie jestem pewien, jaki jest Twój znacznik, ale spróbuj czegoś mniej więcej takiego: {]}

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

Nazwa gracza zostanie zaktualizowana, jeśli podano id, ale zostanie utworzona w inny sposób.

Podejście do definiowania metody autosave_associated_record_for_ jest bardzo interesujące. Na pewno to wykorzystam! Rozważmy jednak również to prostsze rozwiązanie.

 4
Author: Anson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-17 17:04:34

Dla podsumowania sprawy w pytaniu (odnosi się do find_or_create), blok if W odpowiedzi Francois może być przeformułowany jako:

self.author = Author.find_or_create_by_name(author.name) unless author.name.blank?
self.author.save! 
 3
Author: KenB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-25 01:55:43

To działa świetnie, jeśli masz związek has_one lub belongs_to. Ale wypadł z has_many lub has_many przez.

Mam system tagowania, który wykorzystuje związek has_many: through. Żadne z tych rozwiązań nie dało mi miejsca, gdzie musiałem się udać, więc wymyśliłem rozwiązanie, które może pomóc innym. Zostało to przetestowane na Rails 3.2.

Setup

Oto podstawowa wersja moich modeli:

Obiekt Lokalizacji:

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

Tag Obiekty

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

Rozwiązanie

Rzeczywiście nadpisałem metodę autosave_associated_recored_for w następujący sposób:

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

Powyższa implementacja zapisuje, usuwa i zmienia znaczniki tak, jak potrzebowałem podczas używania fields_for w zagnieżdżonej formie. Jestem otwarty na opinie, jeśli istnieją sposoby uproszczenia. Ważne jest, aby zaznaczyć, że jawnie zmieniam znaczniki, gdy etykieta się zmienia, a nie aktualizuję Etykietę.

 3
Author: Dustin M.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-24 18:42:02

A before_validation hook jest dobrym wyborem: jest to standardowy mechanizm powodujący prostszy Kod niż nadpisywanie bardziej niejasnego 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
Author: vemv,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-20 12:01:24

@ dustin-m ' s odpowiedź była dla mnie instrumentalna - robię coś niestandardowego z has_many :przez Związek. Mam temat, który ma jeden Trend, który ma wiele dzieci (rekurencyjny).

ActiveRecord nie lubi, gdy konfiguruję to jako standard has_many :searches, through: trend, source: :children. Pobiera temat.trend i temat.wyszukuje, ale nie robi tematu./ align = "left" / create (nazwa: foo).

Więc użyłem powyższego do skonstruowania niestandardowego autosave i osiągam poprawny wynik z 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
Author: David Hersey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-28 21:54:45

Odpowiedź przez @ François Beausoleil jest niesamowita i rozwiązała duży problem. Świetnie poznać pojęcie autosave_associated_record_for.

Znalazłem jednak jeden narożny przypadek w tej implementacji. W przypadku update istniejącego autora postu(A1), jeśli zostanie przekazana nowa nazwa autora(A2), zmieni się oryginalna(A1) nazwa autora.

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'>

Kod Oringa:

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

Dlatego, że w przypadku edycji {[8] } for post będzie już autorem o id: 1, wejdzie w w przeciwnym razie Zablokuj i zaktualizuj author zamiast tworzyć nową.

Zmieniłem kod (elsif warunek) aby złagodzić ten problem:

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
 0
Author: kiddorails,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-25 12:11:50