简体   繁体   中英

How do you reference only the persisted records in an active record association

In the edit method of many controllers you initialize a new object and edit existing objects

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

However in a view you will often want to cycle through the persisted objects and treat the new object separately

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

The problem

...is that the pages association contains both existing (persisted) pages but also the new page which we made via @new_page = @magazine.pages.new .

It's easy to deal with this however it's ugly

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

I would like to use some assocition method to select only those pages which are persisted:

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

Is there any way of doing this?

您可以在页面模型中创建一个persisted作用域: scope :persisted, -> { where "id IS NOT NULL" } ,这可以避免在每个关联页面上进行迭代,以检查它是否是新记录。

Both the suggestions from @Florent2 and @CDub are sound. However @florent2's suggestion meant hitting the database again (and potentially ditching any preset eager loading which I didn't want to do) and @CDub's suggestion didn't quite work out in terms of code. Here is what I ended up going with:

Returning only persisted records for a particular association

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

this allows you to call .persisted on any ActiveRecord relation of pages associated with Magazine. It doesn't hit the database again as it simply filters through the pre-loaded objects returning the ones which are persisted.

Making the code reusable

Since I want to reuse this code on a regular basis I can pull it out into a module

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

It can then be included into the association methods using a lambda:

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

end

and I can call it intuitively:

@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

You could always reject pages that are new records...

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

where on Page you'd have something like:

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

I approach this problem differently. I do not create a new object in the controller, but instead do so directly in the form.

First, to run through your controller, why are you passing the page_id as your primary params[:id] to your Magazines controller? Looks to me that you want this:

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

Then, in your magazines#edit view, you'd do this:

%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.

In that fields_for line, you're asking for magazine-form fields for pages, but then telling it to only render the fields for a specific, new page, which you are creating on the fly with @magazine.pages.build .

References:
fields_for
Nested Model Form Railscast (See also Part 2)

Another cleaner syntax, using ActiveRecord where.not and still get back an ActiveRecord collection:

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

Rails 4 and 5 answer:

Just put this code in initializer (file in config/initializers directory, with .rb extension):

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

You can now call persisted on any model and association.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM