Custom Attribute Serializers with ActiveRecord in Rails Discussion
This feels much simpler for custom value objects and surprisingly more straightforward than the official way, i.e. registering a new Active Model type.
Great video, thank you.
Chris, could you make a video comparing serialize
(that you demonstrated here) vs attribute
with a custom type (ActiveRecord::Type) vs using composed_of
?
While this may be obvious, but the serialize
method can take 2 types of argument - a class (like Array) or a coder (as coder: CoderClass
).
For a custom class coder
option is necessary. Additionally, the coder Class needs to be added to Rails.application.config.active_record.yaml_column_permitted_classes
during the initialization, unless Rails.application.config.active_record.use_yaml_unsafe_load
is used (which is not ideal.)
When I deal with currency amounts, I always store the amount in the DB as cents and then use some custom methods to generate "_in_dollars" methods that I use on my forms, because most people prefer to write "$1,234.56" dollars instead of "123456" cents.
For my simple currency uses, I think I prefer formatting methods, but if I needed to extract an actual object that I could run methods on from the value, then serialize would make more sense to me.
I've used this model concern for years to handle currency that I store as cents in the DB but my users want to interact with it as dollars:
# app/models/concerns/formatted_currency_in_dollars.rb
module FormattedCurrencyInDollars
extend ActiveSupport::Concern
included do
def self.formatted_currency_in_dollars(model_attribute)
model_attribute = model_attribute.to_s
if model_attribute.ends_with?("_in_cents")
model_attribute = model_attribute.delete_suffix("_in_cents")
end
puts model_attribute
# Getter (cents / 100.0)
define_method("#{model_attribute}_in_dollars") do
cents = send("#{model_attribute}_in_cents")
return nil if cents.blank?
value = (cents.to_f / 100.0)
("%.2f" % value) # Force 2 spaces to right of decimal
end
# Setter (dollars * 100.0)
define_method("#{model_attribute}_in_dollars=") do |value|
cents = (value.blank? ? nil : (value.to_s.gsub(/[^0-9.\-]/, "").to_f * 100)).to_i
send("#{model_attribute}_in_cents=", cents)
end
end
end
end
It assumes you have a DB column named "_in_cents", and it will create the getter/setter for the "_in_dollars". I prefer to be very explicit and store the "_in_cents" on my DB column, so that there's no confusion about what value is in the DB. If you were just looking at the DB values, and you saw "price" and the value was "200", is that "2.00 dollars" or "200 dollars"? Being explicit in situations like this avoids any confusion.