Rails bitwise, enum with super powers
TL;DR
Rails enum field type is good but does not fit for all usages, even more for complex ones, e.g : multi-active states.
Use attr_bitwise instead.
Handle states with ActiveRecord
- integer : reduce maintainability, do not allow to store many values without a lot of “homemade” helper
- array (json encoded): allow multiple values but make SQL queries complex (regex..)
- enum: similar to integer but with rails sugar, do not allow to store many values
- bitwise: declarative, flexible
Enums are very good alternative to handle states with only one active value at once.
There is a limitation when you want to store many states at once, in one place.
Here is the problem we faced at JobTeaser, on a recent project.
We wanted to handle a multi-value enum field for a company configuration record.
Here comes the bitwise
Context
At JobTeaser, every company have the choice to enable many payments modes, in order to publish job offers, let’s call these payment modes:
euros, dollars and bitcoins
The problem
How do you handle with enum or basic SQL fields the fact that a company have euros and dollars payments modes enabled?
And how do you query the following sentence?
Return all companies with payments modes euros and dollars or at least bitcoins
You can define many enum fields, or JSON encoded field, but good luck to query and maintain it.
Our solution
That’s why we built the attr_bitwise library.
Assume you have a Company model with a payment_modes_value field.
class Company < ActiveRecord::Base
include AttrBitwise
attr_bitwise :payment_modes, mapping: [:euros, :dollars, :bitcoins]
end
just with theses few lines, you will be able to do the following :
company = Company.last
company.payment_modes # => [:euros, :dollars]
company.payment_modes_value # => 3
company.payment_mode?(:euros) # => true
company.payment_mode?(:bitcoins) # => false
company.payment_mode == :euros # => false
company.remove_payment_mode(:dollars)
company.payment_mode == :euros # => true
Company.PAYMENT_MODES_MAPPING # => { euros: 1, dollars: 2,
bitcoins: 4 }
Simple, isn’t it?
Let’s now add some scopes to our model in order to allow advanced querying :
class Company < ActiveRecord::Base
include AttrBitwise
attr_bitwise :payment_modes, mapping: [:euros, :dollars, :bitcoins] scope :with_any_payment_modes, lambda { |*p_sym|
where(
payment_modes_value: bitwise_union(*p_sym, 'payment_modes')
)
}
scope :with_all_payment_modes, lambda { |*p_sym|
where(
payment_modes_value:
bitwise_intersection(*p_sym, 'payment_modes')
)
}end
# return all companies that have :euros or :dollars as payment modes
Company.with_any_payment_modes(:euros, :dollars).select(:id)
# return all companies that have :euros and :dollars as payment modes
Company.with_all_payment_modes(:euros, :dollars).select(:id)
You have noticed the use of two low level static methods.
Because theses methods are static, you have to pass the column name as last argument.
bitwise_intersection(*mixed_array, column_name)
This helper allows you to make AND logic operations on the current value passing an array of Fixnum or Symbol.
bitwise_union(*mixed_array, column_name)
This helper allows you to make OR logic operations on the current value passing an array of Fixnum or Symbol.
Theses two helpers allow you to make advanced operations on your bitwise attribute, without limitation!
The library is well-tested and already used in production at JobTeaser with hundred of happy customers!
Please fell free to contribute on github or comment this article!