Reputation: 26048
My model has a string field (a short) and it stores it in db which is fine. But I want it always to return a symbol instead of string and, also, I'd like to assign a symbol to this string attribute. What I'm doing now doesn't work.
class MyModel < ActiveRecord::Base
attr_accessible :attr1
def attr1
# self.try(:attr1).to_sym # how to return symbol?
end
def attr1= value
# super.attr1.to_sym # doesn't work either
end
end
How do I reach this?
Upvotes: 2
Views: 3563
Reputation: 798
I think there is another approach using ActiveRecord's attribute
method
First we need to make a custom ActiveRecord::Type, in this case inheriting from ActiveModel::Type::String. This meets the recommendation from the documentation to extend existing types / inherit from ActiveModel::Type::Value.
# app/lib/core_ext/active_record/type/symbol.rb
module ActiveRecord
module Type
class Symbol < ::ActiveModel::Type::String
def deserialize(obj)
super&.to_sym
end
# defined by ActiveModel::Type::String and we do not need to override for this example
# def serialize; end
end
end
end
Then we need to register this custom type:
# config/initializers/active_record.rb
# Users may also define their own custom types, as long as they respond to the methods defined on the value type.
# The method deserialize or cast will be called on your type object, with raw input from the database or from your
# controllers. See ActiveModel::Type::Value for the expected API.
# It is recommended that your type objects inherit from an existing type, or from ActiveRecord::Type::Value
# https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute
require "./app/lib/core_ext/active_record/type/symbol"
ActiveRecord::Type.register(:symbol, ActiveRecord::Type::Symbol)
Now we can set our symbol type as follows:
class MyModel < ApplicationRecord
attribute :attr1, :symbol
end
Examples
instance = MyModel.new(attr1: "string")
instance => #<MyModel:0x0000ffffacaa19d0 id: 1, attr1: :string>
instance.attr1 => :string
instance.inspect => "#<MyModel id: 1, attr1: :string>"
instance.attributes => {"id"=>1, "attr1"=>:string }
# to confirm the value is getting serialized correctly and stored as a string we can look at the before_type_cast method for the attribute
instance.attr1_before_type_cast => "string"
instance.attributes_before_type_cast => {
"id"=> ...,
"attr1"=> "address",
"created_at"=>2024-05-06 23:47:38.85182 UTC,
"updated_at"=>2024-05-06 23:47:38.85182 UTC
}
Upvotes: 1
Reputation: 31477
I think you only need to overwrite the getter, the setter probably works fine if it is a field.
class MyModel < ActiveRecord::Base
def attr1
attributes['attr1']&.to_sym
end
end
Or you could also create a Serializer:
class SymbolSerializer
def self.dump(obj)
return unless obj
obj.to_s
end
def self.load(text)
return unless text
text.to_sym
end
end
And then in your model:
class MyModel < ActiveRecord::Base
serialize :attr1, SymbolSerializer
end
Upvotes: 9
Reputation: 8853
If you need to do it on multiple columns or in different models I would suggest to generalize the solution:
class MyModel < ActiveRecord::Base
include Concerns::Columnable
treat_as_symbols :attr1
end
module Concerns::Columnable
extend ActiveSupport::Concern
included do
def self.treat_as_symbols *args
args.each do |column|
define_method "#{column}" do
read_attribute(column.to_sym).to_sym
end
end
end
end
end
Upvotes: 2