Accepts_nested_attributes_for с find_or_create?
Я использую Rails-метод accepts_nested_attributes_for с большим успехом, но как я могу заставить его не создавать новые записи, если запись уже существует?
В качестве примера:
Скажем, у меня есть три модели: Team, Membership и Player, и каждая команда имеет игроков из нескольких игроков через членство, а игроки могут принадлежать многим командам. Модель Team может затем принимать вложенные атрибуты для игроков, но это означает, что каждый игрок, представленный в форме комбинированной команды + игрока (ов), будет создан как новая запись игрока.
Как мне делать что-то, если я хочу только создать запись нового игрока таким образом, если еще нет игрока с тем же именем? Если есть игрок с тем же именем, новые записи игроков не должны создаваться, но вместо этого правильный игрок должен быть найден и связан с новой записью команды.
Ответы
Ответ 1
Когда вы определяете крючок для ассоциаций автосохранения, нормальный путь кода пропускается и вместо этого вызывается метод. Таким образом, вы можете сделать это:
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
Этот код не проверен, но он должен быть в значительной степени тем, что вам нужно.
Ответ 2
Не думай об этом, добавляя игроков в команды, думай об этом, добавляя членство в команды. Форма напрямую не работает с игроками. Модель Membership может иметь виртуальный атрибут 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 и может использоваться в любой форме членства.
Примечание. При таком методе модель игрока создается до проверки. Если вы не хотите этого эффекта, сохраните плеер в переменной экземпляра, а затем сохраните его в обратном вызове before_save.
Ответ 3
При использовании :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_
очень интересен. Я обязательно буду использовать это! Однако рассмотрите это более простое решение.
Ответ 4
Чтобы обойти вопрос в терминах вопроса (ссылается на find_or_create), блок if в ответе Франсуа может быть перефразирован как:
self.author = Author.find_or_create_by_name(author.name) unless author.name.blank?
self.author.save!
Ответ 5
Это отлично работает, если у вас есть отношения has_one или belongs_to. Но не хватало has_many или has_many через.
У меня есть система тегов, в которой используется has_many: через отношения. Ни одно из решений здесь не привело меня туда, куда мне нужно было идти, поэтому я придумал решение, которое может помочь другим. Это было протестировано на 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
Вышеприведенная реализация сохраняет, удаляет и изменяет теги так, как мне было нужно, используя поля для вложенной формы. Я открыт для обратной связи, если есть способы упростить. Важно отметить, что я явно меняю теги, когда метка меняется, а не обновляет метку тега.
Ответ 6
A before_validation
hook - хороший выбор: стандартный механизм, обеспечивающий более простой код, чем переопределение более неясного 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
Ответ 7
@dustin-m ответ был полезен для меня - я делаю что-то обычай с has_many: через отношения. У меня есть Тема, в которой есть один Тренд, у которого много детей (рекурсивный).
ActiveRecord не нравится, когда я настраиваю его как стандартное отношение has_many :searches, through: trend, source: :children
. Он извлекает 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
Ответ 8
Ответа на этот вопрос @François Beausoleil is awesome and solve a large problem. Замечательно узнать о концепции 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
для сообщения уже будет автором с 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