使用find_or_create接受_ented_attributes_for?

时间:2010-08-26 22:06:00

标签: ruby-on-rails validation forms activerecord

我正在使用Rails的accepts_nested_attributes_for方法取得了巨大的成功,但是如果记录已经存在,我怎么能创建新记录?

举例来说:

假设我有三个模型,团队,会员和玩家,每个团队都有很多玩家通过会员资格,玩家可以属于很多团队。然后,团队模型可以接受玩家的嵌套属性,但这意味着通过组合团队+玩家形式提交的每个玩家将被创建为新的玩家记录。

如果我只想创建一个新的玩家记录,如果还没有同名的玩家,我应该怎么做呢?如果具有相同名称的玩家,则不应创建新的玩家记录,而应找到正确的玩家并与新的团队记录相关联。

8 个答案:

答案 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