class Setting < ApplicationRecord
serialize :additional_settings, JSON
store(:additional_settings,
accessors: %i[duration_type remind_before],
coder: JSON)
enum duration_type: %i[days hours]
end
> Setting.duration_types
> {"days": 0 ,"hours": 1}
this works fine
But
> a = Setting.first
> #<Setting id: 32, name: "start_date_setting", additional_settings: {"remind_before"=>1, "duration_type"=>1}>
> a.days?
> false
> a.hours?
> false
this doesn't work as expected
and
> a.days!
> (0.5ms) BEGIN
SQL (0.8ms) UPDATE `settings` SET `updated_at` = '2020-05-23 06:09:21', `additional_settings` = '\"{\\\"remind_before\\\":1,\\\"duration_type\\\":\\\"days\\\"}\"' WHERE `settings`.`id` = 32
(2.0ms) COMMIT
this should actually update duration_type as 0 but its updated as "days"
does this work only for integer fields?
There is a way to make enum work with string values. You can do it like this:
enum duration_type: {
days: "days",
hours: "hours"
}
But it won't help to make the code work in this case. The problem here is that ActiveRecord expects enum
to be defined on the attribute and stored in the database as a column. It's not compatible with stores.
Here is an implementation of #days?
: click. As you can see, Rails is checking self[attr]
where attr
is the name of the enum (duration_type
in our case). And self[attr]
is equal to self.attributes[attr]
. For the model Setting
attributes contains only additional_settings
, so no value found, so self.attributes[:duration_type]
gives nil
.
There is a question why a.days!
work without exception in this case then, right? Well, it's tricky. Here is an implementation of this method: click. It's basically a call to update!(attr => value)
where attr
is duration_type
and value is enum's value. Under the hood update!
calls assign_attributes
like this: s.assign_attributes(duration_type: "days")
, - which is equal to s.duration_type = "days"
. And because attr accessor is defined for duration_type
(you specified it in store
call) it writes value to additional_settings
and saves it.
Here is a test to check how it works:
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "activerecord", "6.0.3"
gem "sqlite3"
gem "byebug"
end
require "active_record"
require "minitest/autorun"
require "logger"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :settings do |t|
t.text :additional_settings
end
end
class Setting < ActiveRecord::Base
serialize :additional_settings, JSON
store :additional_settings,
accessors: %i[duration_type remind_before],
coder: JSON
enum duration_type: { days: "days", hours: "hours" }
end
class BugTest < Minitest::Test
def test_association_stuff
s = Setting.new
s.duration_type = :days
s.save!
puts s.attributes
end
end