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

693 Views Asked by At

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?

1

There are 1 best solutions below

0
kimrgrey On BEST ANSWER

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