ruby-on-railsjsonrubyenumsrails-models

Does enum work only for integer fields in ruby on rails?


This is my model

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

additional_settings is a JSON column

> 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?


Solution

  • 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