ruby-on-railsactiverecord

Rails, how to lazily replace an ActiveRecord collection


I am writting a method that reassigns a polymorphic collection like:

class Color < ApplicationRecord
  belongs_to :colorable, polymorphic: true
end

class Table < ApplicationRecord
  has_many :colors, as: :colorable
end

When I reasign the collection the existing elements of the collection have their owner connection fields nullified:

table.colors = new_colors_array

This query is executed:

UPDATE "colors" SET "colorable_id" = $1, "colorable_type" = $2 WHERE "colors"."colorable_id" = $3 AND "colors"."colorable_type" = $4 AND ("colors"."id" = $5 OR "colors"."id" = $6)  [["colorable_id", nil], ["colorable_type", nil], ["colorable_id", "THE_TABLE_ID"], ["colorable_type", "Table"], ["id", "THE_COLOR_ID_1"], ["id", "THE_COLOR_ID_2"]]

And it causes this error:

ActiveRecord::NotNullViolation: PG::NotNullViolation: ERROR:  null value in column "colorable_type" of relation "colors" violates not-null constraint

I could do first a:

table.colors.destroy_all

But the method that contains all this logic is supposed to not make any persistent change in the DB. Allowing developers to make table.save when they are ready.

What would be the strategy to make a collection replacement but still not doing any persistent change in the DB until colorable.save is called?

I have tried with:

table.colors.clear # => touches DB
table.colors = [] # => touches DB
table.colors.reset # => touches DB
table.color.replace(new_colors_array) # => touches DB

Solution

  • ActiveRecord actually has a method to manage these cases:

    We can use it like this:

    def assign_colors(new_colors)
      colors.each do |color|
        color.mark_for_destruction
      end
    
      new_colors.each do |new_color|
        colors << new_color
      end
    end