【问题标题】:accepts_nested_attributes_for with find_or_create?使用 find_or_create 接受_nested_attributes_for?
【发布时间】:2010-08-26 22:06:00
【问题描述】:

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

举例:

假设我有三个模型,Team、Membership 和 Player,每个团队通过会员资格拥有_many 玩家,并且玩家可以属于多个团队。然后 Team 模型可能会接受玩家的嵌套属性,但这意味着通过组合的 team+player(s) 表单提交的每个玩家都将被创建为新的玩家记录。

如果我只想在没有同名玩家的情况下以这种方式创建新玩家记录,我应该怎么做?如果名球员同名,则不应创建新球员记录,而是应找到正确的球员并将其与新的球队记录相关联。

【问题讨论】:

  • 请记住,accepts_nested_attributes_for :some_model 实际上只是定义some_model_attributes= 方法的一种奇特方式,因此如果您不喜欢accepts_nested_attributes_for 的行为方式,您总是可以自己实现该模型。

标签: ruby-on-rails validation forms activerecord


【解决方案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

此代码未经测试,但它应该是您所需要的。

【讨论】:

  • 我也认为正确的是 def autosave_associated_records_for_author。
  • 此方法是否适用于关系的另一端?例如,如果我们有 has_many :authors 怎么办?
  • 我在文档中的任何地方都找不到它,但从它应该被覆盖的代码中很清楚:api.rubyonrails.org/classes/ActiveRecord/…github.com/rails/rails/blob/2-3-stable/activerecord/lib/…
  • 奇怪的是,我无法让它按原样工作。那个 else 分支总是抛出这个错误:SQLite3::ConstraintException: posts.author_id may not be NULL。我通过执行以下操作解决了它:author.save! self.author = author.
  • 这个解决方案不太适合我(Rails 3.2)。我遇到了与@Ashitaka 类似的问题,并且没有为子对象设置 ID。在上面的示例中,您需要在self.author.save! 之后添加一行:self.author_id = self.author.id。 @maletor 你可以投票给它,以便其他人可以看到它 - 它正在“显示更多”下消失 - ty。
【解决方案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,可以用于任何会员形式。

注意:使用这种技术,Player 模型是在验证发生之前创建的。如果您不想要这种效果,请将播放器存储在实例变量中,然后将其保存在 before_save 回调中。

【讨论】:

  • 关于该说明:如果您不希望在验证之前创建播放器,请使用find_or_initialize_by_name(name) 而不是find_or_create_by_name(name)
  • 如果我能对这个答案投票一百万次。这对于复杂的关系和避免深度嵌套非常有帮助!谢谢!
【解决方案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】:

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

【讨论】:

  • 这怎么能处理_destroy?
  • 您可以通过将其添加到 foos.map 块中来处理 _destroyfoo = Foo.where...; if object._destroy; self.foos.delete(foo); return nil; else foo end 注意 self.foos.delete(foo) 仅从连接表中删除关联,并且不会'不要删除 HABTM 关系的 foo 记录。
【解决方案5】:

为了解决问题(参考 find_or_create),弗朗索瓦回答中的 if 块可以改写为:

self.author = Author.find_or_create_by_name(author.name) unless author.name.blank?
self.author.save! 

【讨论】:

  • 在 Rails 4 中:self.author = Author.find_or_create_by (name: author.name) 除非 author.name.blank? self.author.save!
【解决方案6】:

如果您有 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 时所需的方式保存、删除和更改标签。如果有简化的方法,我愿意提供反馈。需要指出的是,我是在标签更改时显式更改标签,而不是更新标签标签。

【讨论】:

  • Dustin...我真的很欣赏这种方法。对我来说真的很好。一个问题:分支#Looks like we are removing... 对我来说没有意义。在什么情况下记录不是new_record?并且也没有ID?
  • @LannyBose 它可以在并发请求下发生
  • @dustin-m 表单/视图如何查找这个?我很感兴趣,因为我遇到了几乎相同的问题stackoverflow.com/questions/37595050/…
【解决方案7】:

@François Beausoleil 的回答很棒,解决了一个大问题。很高兴了解autosave_associated_record_for 的概念。

但是,我在这个实现中发现了一个极端情况。在现有帖子作者(A1)的update的情况下,如果传递了新的作者姓名(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 for post 已经是具有 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

【讨论】:

    【解决方案8】:

    @dustin-m 的回答对我很有帮助 - 我正在用 has_many :through 关系做一些自定义的事情。我有一个主题,它有一个趋势,它有很多孩子(递归)。

    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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-12-24
      • 2023-03-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-04
      相关资源
      最近更新 更多