【问题标题】:How do you reference only the persisted records in an active record association如何仅引用活动记录关联中的持久记录
【发布时间】:2013-11-07 17:34:47
【问题描述】:

在许多控制器的编辑方法中,您初始化一个新对象并编辑现有对象

class MagazinesController < ApplicationController
   def edit
      @magazine = Magazine.find(params[:magazine_id])
      @page = Page.find(params[:id])
      @new_page = @magazine.pages.new
   end
end

但是在视图中,您通常会希望循环遍历持久对象并单独处理新对象

# magazines#edit
%h4 Existing pages
- @magazine.pages.each do |page|
  %p= link_to page, page.title

问题

...pages 关联既包含现有(持久)页面,也包含我们通过 @new_page = @magazine.pages.new 创建的新页面。

这很容易处理,但它很丑

%h4 Existing pages
- @magazine.pages.each do |page|
  - if page.persisted?
    %p= link_to page, page.title

我想使用一些关联方法来只选择那些被持久化的页面:

%h4 Existing pages
- @magazine.pages.persisted.each do |page|
  %p= link_to page, page.title

有什么办法吗?

【问题讨论】:

  • 你试过@magazine.pages.select(&amp;:persisted?)

标签: ruby-on-rails activerecord


【解决方案1】:

您可以在您的 Page 模型中创建一个 persisted 范围:scope :persisted, -&gt; { where "id IS NOT NULL" },这样可以避免在每个关联页面上进行迭代以检查它是否是新记录。

【讨论】:

  • 弗洛伦特我也是这么想的。作为一种长期方法,我唯一担心的是它会命中数据库,鉴于记录已经在内存中的数组中,这似乎没有必要。但是,我想知道正确的方法是否可能是在模型上创建.persisted 方法,然后进行映射def persisted pages.collect{|page| page if page.persisted?}。你怎么看?当然,缺点是它不再是 AR 关系,好消息是它很快。 (顺便说一句,我错过了我们的代码会议;)
  • 啊,我知道我的建议正是@CDub 的建议!
  • 不知道你的观点到底如何,但如果%h4 Existing pages 这是你第一次在页面上迭代,那很好。此时页面还没有在内存中,@magazine.pages 只是一个延迟加载的 ActiveRelation 对象,因此添加 .persisted 将仅更改执行的 SQL 查询以获取记录(通过在 @987654328 上添加额外的条件@),不会产生额外的 SQL 请求(我也想念我们的代码 convos :))。
  • 好点 - 我忘了延迟加载。出于兴趣,您更喜欢哪条路线 - 过滤数组或更改查询?
  • 如果我只在一个视图中面对这个问题,我可能会在- @magazine.pages.each do |page| 之后放置- next if page.new_record?,(我承认这很丑:))否则将使用persisted 范围。 Rails 在数组中注入新记录太糟糕了:S 编辑:似乎您可以使用本机 scoped 而不是自定义 persisted 来实现相同的结果(但语义并不简单)。
【解决方案2】:

@Florent2 和@CDub 的建议都是合理的。然而,@florent2 的建议意味着再次访问数据库(并且可能会放弃我不想做的任何预设的急切加载),而@CDub 的建议在代码方面并没有完全奏效。这是我最终的结果:

仅返回特定关联的持久记录

class Magazine < ActiveRecord::Base
  has_many :pages do 
    def persisted
      collect{ |page| page if page.persisted? }
    end
  end
end

这允许您在与杂志关联的页面的任何 ActiveRecord 关系上调用 .persisted。它不会再次访问数据库,因为它只是过滤预加载的对象,返回持久化的对象。

使代码可重用

因为我想定期重用这段代码,我可以把它拉到一个模块中

module PersistedExtension
  def persisted
    select{|item| item if item.persisted?}
  end
end

然后可以使用 lambda 将其包含到关联方法中:

class Magazine < ActiveRecord::Base
  # ...
  has_many :pages, -> { extending PersistedExtension }

end

我可以直观地称呼它:

@magazine = Magazine.first

@magazine.pages.persisted
# => array of pages which are persisted

# the new persisted association extension works on any AR result set
@magazine.pages.order('page ASC').persisted

【讨论】:

  • 我需要压缩数组,因为我得到了 nil 返回。 collect{ |page| page if page.persisted? }.compact
  • @dazonic select(&amp;:persisted?) 返回相同的结果并且看起来更干净。见documentation for Enumerable#select
【解决方案3】:

您总是可以拒绝新记录的页面...

%h4 Existing pages
- @magazine.pages.persisted.each do |page|
    %p= link_to page, page.title

Page 上,你会有类似的东西:

def self.persisted
  reject {|page| page.new_record? }
end

【讨论】:

  • 这正是我所想的。缺点是它不再是 AR 关系,优点是如果数据已经在内存中,它不会再打扰 DB。
【解决方案4】:

另一种更简洁的语法,使用 ActiveRecord where.not 并仍然返回 ActiveRecord 集合:

- @magazine.pages.where.not(id: nil).each do |page|
    ...

【讨论】:

    【解决方案5】:

    我以不同的方式处理这个问题。我没有在控制器中创建新对象,而是直接在表单中创建。

    首先,要运行您的控制器,为什么要将 page_id 作为您的主要params[:id] 传递给您的杂志控制器?在我看来你想要这个:

    class MagazinesController < ApplicationController
       def edit
          @magazine = Magazine.find(params[:id]).includes(:pages)
       end
    end
    

    然后,在您的 magazines#edit 视图中,您会这样做:

    %h4 Existing pages
    - @magazine.pages.each do |page|
      %p= link_to page, page.title
    
    = form_for @magazine do |f|
      = f.fields_for :pages, @magazine.pages.build do |builder|
        = builder.text_field :title
        # etc.
    

    fields_for 行中,您要求页面的杂志表单字段,然后告诉它只呈现特定的新页面的字段,您正在使用@magazine.pages.build 动态创建该页面。

    参考:
    fields_for
    Nested Model Form Railscast(另见第 2 部分)

    【讨论】:

    • 嗨,卡洛斯。这绝对是一种合理的方法,但是我总是更喜欢在控制器中实例化对象而不是在视图中。我永远不记得在这种情况下是否有后果,但总的来说感觉更干净。顺便说一句,我实际上使用 cancan 来 load_and_authorize_resource - 我把加载对象的东西放在那里只是为了说明(附注:我喜欢你的目标博文)
    • 另外,您只将问题推到下游。一旦您实例化了新对象,它仍然会影响引用.pages 关联的任何其他内容。唯一的区别是它只查看它的下游代码会受到影响
    • 嗯,你把问题推到下游是对的。有一个依赖于视图中元素顺序的实现是不好的。也就是说,我不喜欢一个控制器操作负责多个类上的多个实例变量。这样,在我的代码中,通常表明我正在错误地处理问题。
    • 您是否有机会通过表单链接使用 javascript-action 来实例化页面对象的新字段?这样,Ruby 根本不负责创建新对象,@magazine.pages 永远不会受到污染。在此处查看link_to_add_fieldsrailscasts.com/episodes/197-nested-model-form-part-2
    • 我可能会做这样的事情,但除非有充分的理由不这样做,否则我总是更喜欢在没有 JS 的情况下解决问题,然后再加强。我觉得在这种情况下使用 JS 只会弄得一团糟——这是一个普遍的问题
    【解决方案6】:

    Rails 4 和 5 答案:

    只需将此代码放入初始化程序(config/initializers 目录中的文件,扩展名为.rb):

    module MyApp
      module ActiveRecordExtensions
        extend ActiveSupport::Concern
    
        class_methods do
    
          def persisted
            select(&:persisted?)
          end
    
        end
      end
    end
    
    ActiveSupport.on_load :active_record do
      include MyApp::ActiveRecordExtensions
    end
    

    您现在可以在任何模型和关联上调用persisted

    【讨论】:

    • 你不应该直接访问ActiveRecord::Base,而是使用负载钩子:ActiveSupport.on_load :active_record { include MyApp::ActiveRecordExtensions }
    • 是的,不要强迫ActiveRecord::Base 过早加载是个好主意。 Here is a blog post 解释原因。
    猜你喜欢
    • 2016-05-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-26
    • 1970-01-01
    相关资源
    最近更新 更多