我正在使用Rails的accepts_nested_attributes_for方法取得了巨大的成功,但是如果记录已经存在,我怎么能不创建新记录?
举例来说:
假设我有三个模型,团队,会员和玩家,每个团队都有很多玩家通过会员资格,玩家可以属于很多团队。然后,团队模型可以接受玩家的嵌套属性,但这意味着通过组合团队+玩家形式提交的每个玩家将被创建为新的玩家记录。
如果我只想创建一个新的玩家记录,如果还没有同名的玩家,我应该怎么做呢?如果是具有相同名称的玩家,则不应创建新的玩家记录,而应找到正确的玩家并与新的团队记录相关联。
答案 0 :(得分:53)
为自动保存关联定义挂钩时,将跳过正常的代码路径,而是调用您的方法。因此,您可以这样做:
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
此代码未经测试,但它应该是您所需要的。
答案 1 :(得分:30)
不要将其视为向球队添加球员,将其视为为球队添加会员资格。该表单不能直接与玩家一起使用。 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,可以用于任何会员形式。
注意:使用此技术,在验证发生之前创建Player模型。如果您不想要此效果,请将播放器存储在实例变量中,然后将其保存在before_save回调中。
答案 2 :(得分:4)
使用: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 :(得分:3)
只是为了解决问题(参见find_or_create),弗朗索瓦答案中的if块可以改为:
self.author = Author.find_or_create_by_name(author.name) unless author.name.blank?
self.author.save!
答案 4 :(得分:3)
如果您有has_one或belongs_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时,上述实现以我需要的方式保存,删除和更改标记。如果有方法可以简化,我愿意接受反馈。重要的是要指出,我在标签更改时明确更改标签,而不是更新标签标签。
答案 5 :(得分:3)
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
答案 6 :(得分:1)
@FrançoisBeausoleil回答很棒,解决了一个大问题。很高兴了解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'>
Oringinal code:
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的作者,它将进入else,阻止并将更新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
条件)以缓解此问题:
class LandingPage extends React.Component {
render() {
return (
<h2>Hello</h2>
);
}
}
ReactDOM.render(<LandingPage />, document.getElementById('app'));
答案 7 :(得分:0)
@ dustin-m的答案对我有帮助 - 我正在做一些与has_many有关的事情:通过关系。我有一个主题有一个趋势,有很多孩子(递归)。
当我将此配置为标准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