Table-based enums for ActiveRecord
Ruby
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
lib
test
.gitignore
.travis.yml
CHANGELOG
Gemfile
Gemfile.ar3
LICENSE
README.markdown
Rakefile
enum_table.gemspec

README.markdown

Enum Table Build Status

Table-based enumerations for ActiveRecord.

Deprecation Notice

ActiveRecord 4.1 introduced its own way of handling enumerations, which is somewhat at odds with the approach of this gem. To get the additional benefits of Enum Table, it's suggested that you migrate to standard ActiveRecord enums, and then sync the enum values in your code to your database as necessary.

Enum Table is compatible with ActiveRecord versions up to 4.1.x, but 4.2.x support is not currently planned.

What?

When you have a column that should only take one of a finite set of string values (e.g., gender, statuses through some workflow), it's usually best not to store these as strings. Many databases, such as MySQL and PostgreSQL have native enum types which effectively let you treat these as strings while storing them internally using as few bytes as possible. Indeed there are already plugins that let you use these native enum types.

But sometimes this is inadequate.

Most obviously, not all databases have a native enum type, notably SQLite.

Further, in the case of MySQL, the enum type leaves a lot to be desired in a production setting. If you need to do anything to the list of allowed values other than adding values to the end of the list, MySQL will rebuild the entire table, which can be very slow. Unless you're using something like pt-online-schema-change, it will also lock the table during this period, which could be unacceptable for large tables.

A common alternative is to simply keep the value-to-id mapping in the application, hardcoded in the model. The downside of this is that your database is no longer self-documenting: the integers mean nothing without the value mapping buried in your application code, making it hard to work with the database directly. Another problem is the database cannot enforce any referential integrity.

This plugin implements a different strategy which solves the above problems - each enum is defined by a table with id and value columns, which defines the values and the integers they map to. Altering the values can be done with simple DML statements, which do not require rebuilding any tables.

Usage

Create your enum tables in migrations. Example:

create_enum_table :user_genders do |t|
  t.add :male
  t.add :female
end

Then add the enum ID to your model table:

add_column :users, :gender_id, null: false

Then in your model:

class User < ActiveRecord::Base
  enum :gender
end

Note the convention: for a model User with enum gender, the column is users.gender_id, and the enum_table is user_genders. You can override these with the :id_name and :table options:

enum :gender, id_name: :sex_id, table: :sexes

Custom columns

While the names id and value are fixed, you can change other attributes of the column. For example, the ID has limit: 1 by default, but you can change this if you have a large list of enum values:

create_enum_table :user_countries do |t|
  t.id limit: 2
  t.add 'Afghanistan'
  t.add 'Albania'
  # ...
end

Similarly you can customize the value column, say if you want to place a varchar limit:

create_enum_table :user_countries do |t|
  t.value limit: 100
  # ...
end

Updating enums

To change the list of enums:

change_enum_table :user_genders do |t|
  t.add :other
  t.remove :male
end

To drop an enum table:

drop_enum_table :user_genders

Under the hood, create_enum_table and drop_enum_table maintain the list of enum tables in the enum_tables table. This allows the table data to be tracked by db/schema.rb so it gets copied to your test database.

Hardcoded mappings

If you really want, you can forego the table completely and just hardcode the ids and values in your model:

enum :genders, table: {male: 1, female: 2}

Or since our IDs are 1-based and sequential:

enum :genders, table: [:male, :female]

Of course, by not using tables, you lose some of the advantages mentioned earlier, namely a self-documenting database and referential integrity.

Values

By default, user.gender will be either the symbol :male or :female. If you're transitioning from using old-fashioned varchars, however, you may find it less disruptive to use strings instead. Do that with the :type option:

enum :genders, type: :string

I can't set up my Rails app!

If you load any models during Rails' initialization phase (e.g. from config/application.rb, or an initializer), and these models depend on the existence of enum tables, you may wind up with a circular dependency: your app startup depends on an enum table existing, and creating the enum table requires loading the app (via db:schema:load). This might only manifest itself when you try to load your schema on an empty database.

It is strongly recommended to avoid loading models during initialization. However if you cannot avoid this there is an escape hatch:

config.enum_tables.allow_missing_tables = true

This will silently return an empty enum mapping while this flag is true. Of course, this may have knock on effects if your model expects certain values to be present. Consider yourself warned!

Contributing

  • Bug reports
  • Source
  • Patches: Fork on Github, send pull request.
    • Include tests where practical.
    • Leave the version alone, or bump it in a separate commit.

Running tests

testrb test

Copyright

Copyright (c) HowAboutWe. See LICENSE for details.