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:
after_initialize is triggered after the model is loaded also
rate_type is nil (as loaded from DB), thus !try(:rate_type) is true
- 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.
The case where it got me is somewhat convoluted (though real), yet I believe that it exposes some conceptual problem.
The case is this:
Since Rails 7.1, the default behavior of
attr_readonlyis to raise when#rate_type=is used on already persisted model. And that's wherestr_enumintroduces a problem:The culprit is here:
What we have here is:
after_initializeis triggered after the model is loaded alsorate_typeisnil(as loaded from DB), thus!try(:rate_type)istruestr_enumtries to doself.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_initializeif 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.