Skip to content

Possibly incorrect after_initialize usage #14

@zverok

Description

@zverok

The case where it got me is somewhat convoluted (though real), yet I believe that it exposes some conceptual problem.

The case is this:

class Rate < ApplicationModel
  # columns are: rate_type, rate_value, rate_currency, etc.

  str_enum :rate_type, %w[fixed hourly], allow_nil: true, default: nil
  attr_readonly :rate_type
end

Since Rails 7.1, the default behavior of attr_readonly is to raise when #rate_type= is used on already persisted model. And that's where str_enum introduces a problem:

rate = Rate.create!(rate_type: nil, rate_value: 10)

Rate.find_by(rate.id) # ActiveRecord::ReadonlyAttributeError: rate_type

The culprit is here:

        after_initialize do
          send("#{column}=", default_value) if has_attribute?(column) && !try(column)
        end

What we have here is:

  1. after_initialize is triggered after the model is loaded also
  2. rate_type is nil (as loaded from DB), thus !try(:rate_type) is true
  3. thus, str_enum tries to do self.rate_type = nil (which would be pointless anyway), triggering the "it tries to write to readonly column".

I believe the most comprehensive fix is to NOT set anything in after_initialize if it is an initialization of a model loaded from DB (i.e. if persisted?). As far as I understand, the intention of this block is that when the new record is initialized, the default should be set.

Or I might be missing something.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions