has_many with unsaved default in collection

Viewed 93

I thought it would be a simple thing, but unfortunately I can't find a way to create a collection with a default entry.

There is the model Entry which can be filtered by one or more filter_entries. There should be a fallback, when there is no association. Cause I dont't want to create a default association in the database for 100000s of entries.

Currently I made an array, which returns a default entry.

class Entry
  belongs_to :object
  has_many :entry_filters, dependent: :destroy

  def entry_filters
    super.presence || [EntryFilter.new(filter: default_filter)]
  end
  
  private

  def default_entry
    object.filters.find_by(default: true)
  end
end

An entry with an association

Entry.first.entry_filters
=> [#<EntryFilter:0x00007f4b39cb16d8
  id: 1,
  entry_type: "Entry",
  entry_id: 1,
  filter_id: 1>]
Entry.first.entry_filters.class
=> EntrySegment::ActiveRecord_Associations_CollectionProxy

An entry without an association

Entry.second.entry_filters
=> [#<EntryFilter:0x000055e9060ac608
  id: nil,
  entry_type: nil,
  entry_id: nil,
  filter_id: 1>]
Entry.second.entry_filters.class
=> Array

It works, but I want to work with a collection, like when there is a real association linked to the entry.

Is there a way, using the ActiveRecord::Associations::HasManyAssociation to fake that collection? I've tried several methods, but each will create thaht association instead of building it.

Entry.second.association(:entry_filters).writer([EntryFilter.new(filter: Filter.first)])
=> [#<EntryFilter:0x00007f90d0b38d88 id: 2, entry_type: "Entry", entry_id: 2, filter_id: 1>]
Entry.second.association(:entry_filters).replace([EntryFilter.new(filter: Filter.first)])
=> [#<EntryFilter:0x00007f90d0a457c8 id: 3, entry_type: "Entry", entry_id: 2, filter_id: 1>]
Entry.second.association(:entry_filters).concat([EntryFilter.new(filter: Filter.first)])
=> [#<EntryFilter:0x00007f90d0a637a5 id: 4, entry_type: "Entry", entry_id: 2, filter_id: 1>]
1 Answers

You can use the .build collection method to do what you want:

def entry_filters
  filters = super
  filters.build(filter: default_filter) if filters.blank?
  filters
end

The .build method instantiates a new associated object (without saving), sets the foreign key, and adds it to the object's collection in memory. In ordinary usage you would call it like <entry_obj>.entry_filters.build(filter: ...). But, you can't do that inside .entry_filters itself or you end up with infinite recursion. Hence, we call super first to get the collection and then call .build if the collection is currently empty.

> Entry.new.entry_filters
=> #<ActiveRecord::Associations::CollectionProxy [#<EntryFilter id: nil, 
filter_id: ... ]

When you call .save on the parent entry, it will also save the new entry filter at that point.

Related