accepts_nested_attributes_for с find_or_create? - PullRequest
53 голосов
/ 27 августа 2010

Я использую Rails метод acceptpts_nested_attributes_for с большим успехом, но как его получить не создавать новые записи, если запись уже существует?

В качестве примера:

Скажем, у меня есть три модели: Team, Membership и Player, и у каждой команды есть _many игроков через членство, и игроки могут принадлежать ко многим командам.В этом случае модель команды может принимать вложенные атрибуты для игроков, но это означает, что каждый игрок, представленный через комбинированную форму «команда + игрок (и)», будет создан как новая запись игрока.

Как мне поступить, выполняя определенные действияесли я хочу создать новую запись игрока таким образом, если еще нет игрока с таким именем?Если является игроком с тем же именем, новые записи игрока не должны создаваться, но вместо этого должен быть найден правильный игрок и связан с новой записью команды.

Ответы [ 8 ]

54 голосов
/ 27 августа 2010

Когда вы определяете ловушку для ассоциаций автосохранения, нормальный путь кода пропускается, и вместо этого вызывается ваш метод. Таким образом, вы можете сделать это:

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

Этот код не проверен, но он должен быть в значительной степени тем, что вам нужно.

30 голосов
/ 14 мая 2011

Не думайте об этом как о добавлении игроков в команды, думайте об этом как о добавлении членства в команды.Форма не работает с игроками напрямую.Модель членства может иметь виртуальный атрибут 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 к любому конструктору форм членства.не относится к acceptpts_nested_attributes_for и может использоваться в любой форме членства.

Примечание. С помощью этого метода модель Player создается до проверки.Если вам не нужен этот эффект, сохраните проигрыватель в переменной экземпляра, а затем сохраните его в обратном вызове before_save.

4 голосов
/ 17 февраля 2012

При использовании :accepts_nested_attributes_for отправка id существующей записи приведет к тому, что ActiveRecord обновит существующую запись вместо создания новой записи. Я не уверен, на что похожа ваша разметка, но попробуйте что-то вроде этого:

<%= 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_ очень интересен. Я обязательно воспользуюсь этим! Однако рассмотрим и это более простое решение.

3 голосов
/ 20 ноября 2015
Хук

A 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 голосов
/ 24 сентября 2014

Это прекрасно работает, если у вас есть отношения has_one или assign_to. Но потерпел неудачу с помощью has_many или has_many.

У меня есть система тегов, которая использует отношение 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 голосов
/ 25 февраля 2011

Просто для округления в терминах вопроса (относится к find_or_create) блок if в ответе Франсуа можно перефразировать как:

self.author = Author.find_or_create_by_name(author.name) unless author.name.blank?
self.author.save! 
1 голос
/ 25 октября 2017

Ответ @ Франсуа Босолей потрясающий и решил большую проблему. Здорово узнать о понятии autosave_associated_record_for.

Тем не менее, я нашел один угловой случай в этой реализации. В случае update автора существующего сообщения (A1), если передается имя нового автора (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 для записи уже будет автором с идентификатором: 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
0 голосов
/ 29 декабря 2016

@ dustin-m мне очень помог ответ - я делаю что-то особенное с отношениями has_many: through.У меня есть тема с одним трендом, у которой много детей (рекурсивная).

ActiveRecord не нравится, когда я настраиваю это как стандартное has_many :searches, through: trend, source: :children отношение.Он извлекает topic.trend и topic.searches, но не делает topic.searches.create (имя: 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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...