Enums - inconsistent

enum user_type: [:normal,:invited]

in a model method:

if user.user_type == :normal
	#Do Something
end

but after a database call I need

User.find_each do |user|
	if user.user_type.to_sym == :normal
		#Do Something
	end
end

Am I doing it wrong?

1 Like

Take a look at this

Rails will generate predicate methods for you automatically so you could use user.invited? or user.normal?

Let me know if that solves it!

1 Like

@pinzonjulian that gets you part way there.

however, if this is the answer, then the guide should really have a warning saying

Always use .normal? rather than = :normal because…

and of course, that doesn’t handle the case

if user.user_type.in?([:normal,:special]) {
#
}

this isn’t a biggie, but it seems to fit in the category of ‘Does Rails do the obvious thing - or do you need to know the magic’

@Confused_Vorlon maybe that was the case in older versions, but I just tried reproducing it with Rails 6 and the enum values are always casted to String: enum.rb · GitHub

Interesting. I’m on rails 5.2

RSpec.describe Order, type: :model do

  describe "enum" do
    it "to_sym enables comparison" do
      o1 = create(:order)

      expect(o1.status.to_sym).to eq :building
    end

    it "doesn't like direct comparison" do
      o1 = create(:order)

      expect(o1.status).to eq :building
    end
  end
end

just double checked with the above. Test 2 fails.

perhaps this is a 5 vs 6 thing

I did try to upgrade to 6, but just got lost in the thicket of failing dependencies and lack of any clear guide (that I could find) showing what I needed to know about webpacker

I can’t reproduce on 5.2 either: enum_52.rb · GitHub

Whatever what value I assign, enums values are casted to String (or AS::StringInquirer rather).

But your test shows that you expect the attribute to be a symbol. It’s not, it never is, it’s a string, always.

That’s probably where the confusion lies. Maybe AS::StringInquirer.new("foo") == :foo should work though, or maybe it should throw an error to tell you not to compare with symbols. I can see how this is a gotcha.

I may well be doing something odd here.

I have

enum user_type: [:normal,:invited,:dummy]

and in my database, the enum is an Int

add_column :users, :user_type, :integer, null: false, default: 0

my expectation is actually that I don’t care what the enum type is.

I have defined my enums as :normal, :invited, :dummy and I should be able to use them without worrying about how they are implemented

Yeah I get it. Since you use symbols to define the possible values, you expect the model attribute to give you a Symbol. But actually it returns a String.

It’s not a bug per says, as it does what the documentation says.

But I do understand how it doesn’t fit with your expectations. It’s an interesting feedback, and thanks for going through with me to help me understand what your problem was.

just to confirm - I built this on 6.03 and get the same behaviour

require 'rails_helper'

RSpec.describe User, type: :model do

	describe "enums" do
		it "gives normal method" do
			create_user
			expect(@user.normal?).to be true
		end

		it "compares with a sym" do
			create_user
			expect(@user.user_type.to_sym).to eq :normal
		end

		#this test passes - which seems crazy
		it "doesn't compare directly" do
			create_user
			expect(@user.user_type).not_to eq :normal
		end
	end

  def create_user
  	@user = User.new(user_type: :normal)
  	@user.save
  end
end

as you say - I’m not saying this is a bug. Just something that (to me at least) is a wtf

for comparison - without telling any programmer what the language is below, I expect pretty much all of them expect the ‘mysteriousResult’ to be true

enum UserType {
    case normal
    case special
}

struct User {
    var type:UserType
}

let user = User(type: .normal)
let mysteriousResult = (user.type == .normal)

(it’s Swift, and of course it is true)

I agree this is confusing, of course this isn’t the only case where you get confused with symbols / strings but it’s a damn fine example. Here’s another head scratcher I had lately ActiveRecord Attributes is better returning HashWithIndifferentAccess · Issue #38621 · rails/rails · GitHub. I’m sure I can find more cases of confusion. Symbols is one of those things that plague beginners and seniors a like. I do wish we would think about Symbols a bit more and how we can reduce the pains in Rails.

Hi, Here is my issue with enum. When calling user.invited! it will instantly perform an update query. How can we use this user.invited! and after set other attributes and perform only one update query?

Let say I could block user, and I have a function to do it

def block(user)
  user.blocked! # This will perform one update query
  user.blocked_at = Time.now
  user.save # This will perform one update query
end

If I want to perform only one query I have to do

def block(user)
  user.access_type = "blocked" # This will just set the access_type value to blocked
  user.blocked_at = Time.now
  user.save # This will perform one update query
end

here is a gist trying to explain the issue.

This is not really documented here the only way to set an enum attribute is using the bang method.

There is maybe something I’m missing or I’m using it the wrong way.

1 Like

You’re correct.

One option could be to add a set_blocked method that is similar to blocked! but calls assign_attributes instead of update!.

I think one of the strong aims behind ActiveRecord enums is to define an interface that decouples your application’s understanding of that field and the raw value in the database. The raw value could be a string, it could be a number, or it could be a database native enum which may not be represented well in Rails.

So let’s assume we have a status field which uses the following values for some arcane technical reason:

[
  213123, // normal
  238129, // special
  989342 // extra_special
]

How would we work with this field? Sprinkling magic numbers all over our app probably isn’t the best way to deal with it, eg.

do_something if record.status == 213123
record.update!(status: 213123)
Record.where(status: 213123)

What we need is a lookup, ie. something that maps 213123 to our concept of normal. And that’s exactly what AR enums do:

enum status {
  normal: 213123, 
  special: 238129, 
  extra_special: 989342
}

Now instead of using raw values, we use the lookup map everywhere else in our application

do_something if record.status == Record.statuses[:normal]
record.update(status: Record.statuses[:normal])
record.where(status: Record.statuses[:normal])

These cases are actually common enough that Rails provides helpers so that you don’t need to type Record.statuses[:normal] every single time

do_something if record.normal?
record.normal!
Record.normal

@ferngus that’s well put, but most of the languages I know provide let you use the token to represent the magic number

//swift
enum Magic {
case normal = 213123
}

and then we can still write

if user.type = .normal {
//do something
}

in your example, Rails has assigned the token normal for 213123, but you can’t use it - you have to use the auto-generated methods

I think there’s some ambiguity here between accessing the underlying value stored in the database and the semantic symbol this value represents.

ActiveRecord’s standard method of accessing the value of an attribute stored in the database is to call model.<attribute>. That accessor does the same thing regardless of whether the column is enumerated.

Now, we could change AR to default to returning the semantic symbol for that value when that attribute is enumerated. Question here is, how would you then easily access the underlying value?

You could call user.attributes["status"] to get the raw text value of course, but that’s not intuitive for the majority of people who don’t have a clear understanding of ActiveRecord’s enum functionality.

So maybe it’s not a good idea to return the semantic symbol by default. But you’d prefer to write user.status == :active instead of user.status == User.statuses[:active]. No problem, perhaps you can extend ActiveRecord a little bit to achieve that.

class User < ApplicationRecord
  def status
    super.to_sym
  end
end

I’d say the point of an enum is that you mostly don’t access the underlying value.

To my mind, the point of an enum is that we’re hiding the underlying value as an implementation detail.

With that understanding, then it doesn’t matter too much how obscure the access method is.

It could be that there is an autogenerated method which gives you the raw value.

so for example:

user.type = :normal

user.type // :normal
user.type == :normal //true
user.normal? //true
user.type_underlying_value //213123

the giant wtf at the moment for me is

user.type = :normal
..some code
user.type == :normal //false!!!