Save migrations and columns by storing multiple booleans in a single integer.
e.g. true-false-false = 1, false-true-false = 2, true-false-true = 5 (1,2,4,8,..)
class User < ActiveRecord::Base
include Bitfields
bitfield :my_bits, 1 => :seller, 2 => :insane, 4 => :sensible
end
user = User.new(seller: true, insane: true)
user.seller # => true
user.sensible? # => false
user.my_bits # => 3
- records bitfield_changes
user.bitfield_changes # => {"seller" => [false, true], "insane" => [false, true]}
(alsoseller_was
/seller_change
/seller_changed?
/seller_became_true?
/seller_became_false?
)- Individual added methods (i.e,
seller_was
,seller_changed?
, etc..) can be deactivated withbitfield ..., added_instance_methods: false
-
Note: ActiveRecord 5.2 changes the behavior of
_was
and_changed?
methods when used in the context of anafter_save
callback.- ActiveRecord 5.1 will use the use the values that were just changed.
- ActiveRecord 5.2, however, will return the current value for
_was
andfalse
for_changed?
since the previous changes have been persisted.
- Individual added methods (i.e,
- adds scopes
User.seller.sensible.first
(deactivate withbitfield ..., scopes: false
) - builds sql
User.bitfield_sql(insane: true, sensible: false) # => '(users.my_bits & 6) = 1'
- builds sql with OR condition
User.bitfield_sql({ insane: true, sensible: true }, query_mode: :bit_operator_or) # => '(users.my_bits & 2) = 2 OR (users.bits & 4) = 4'
- builds index-using sql with
bitfield ... , query_mode: :in_list
andUser.bitfield_sql(insane: true, sensible: false) # => 'users.my_bits IN (2, 3)'
(2 and 1+2) often slower than :bit_operator sql especially for high number of bits - builds update sql
User.set_bitfield_sql(insane: true, sensible: false) == 'my_bits = (my_bits | 6) - 4'
- faster sql than any other bitfield lib through combination of multiple bits into a single sql statement
- gives access to bits
User.bitfields[:my_bits][:sensible] # => 4
- converts hash to bits
User.bitfield_bits(seller: true) # => 1
Install
gem install bitfields
Migration
ALWAYS set a default, bitfield queries will not work for NULL
t.integer :my_bits, default: 0, null: false
# OR
add_column :users, :my_bits, :integer, default: 0, null: false
Instance Methods
Global Bitfield Methods
Method Name | Example (user = User.new(seller: true, insane: true ) |
Result |
---|---|---|
bitfield_values |
user.bitfield_values |
{"seller" => true, "insane" => true, "sensible" => false} |
bitfield_changes |
user.bitfield_changes |
{"seller" => [false, true], "insane" => [false, true]} |
Individual Bit Methods
Model Getters / Setters
Method Name | Example (user = User.new ) |
Result |
---|---|---|
#{bit_name} |
user.seller |
false |
#{bit_name}= |
user.seller = true |
true |
#{bit_name}? |
user.seller? |
true |
Dirty Methods:
Some, not all, ActiveRecord::AttributeMethods::Dirty
and ActiveModel::Dirty
methods can be used on each bitfield:
Before Model Persistence
Method Name | Example (user = User.new ) |
Result |
---|---|---|
#{bit_name}_was |
user.seller_was |
false |
#{bit_name}_in_database |
user.seller_in_database |
false |
#{bit_name}_change |
user.seller_change |
[false, true] |
#{bit_name}_change_to_be_saved |
user.seller_change_to_be_saved |
[false, true] |
#{bit_name}_changed? |
user.seller_changed? |
true |
will_save_change_to_#{bit_name}? |
user.will_save_change_to_seller? |
true |
#{bit_name}_became_true? |
user.seller_became_true? |
true |
#{bit_name}_became_false? |
user.seller_became_false? |
false |
After Model Persistence
Method Name | Example (user = User.create(seller: true) ) |
Result |
---|---|---|
#{bit_name}_before_last_save |
user.seller_before_last_save |
false |
saved_change_to_#{bit_name} |
user.saved_change_to_seller |
[false, true] |
saved_change_to_#{bit_name}? |
user.saved_change_to_seller? |
true |
-
Note: These methods are dynamically defined for each bitfield, and function separately from the real
ActiveRecord::AttributeMethods::Dirty
/ActiveModel::Dirty
methods. As such, generic methods (e.g.attribute_before_last_save(:attribute)
) will not work.
Examples
Update all users
User.seller.not_sensible.update_all(User.set_bitfield_sql(seller: true, insane: true))
Delete the shop when a user is no longer a seller
before_save :delete_shop, if: -> { |u| u.seller_change == [true, false] }
List fields and their respective values
user = User.new(insane: true)
user.bitfield_values(:my_bits) # => { seller: false, insane: true, sensible: false }
TIPS
- [Upgrading] in version 0.2.2 the first field(when not given as hash) used bit 2 -> add a bogus field in first position
- [Defaults for new records] set via db migration or name the bit foo_off to avoid confusion, setting via after_initialize does not work
- It is slow to do:
#{bitfield_sql(...)} AND #{bitfield_sql(...)}
, merge both into one hash - bit_operator is faster in most cases, use
query_mode: :in_list
sparingly - Standard mysql integer is 4 byte -> 32 bitfields
- If you are lazy or bad at math you can also do
bitfields :bits, :foo, :bar, :baz
- If you are want more readability and reduce clutter you can do
bitfields 2**0 => :foo, 2**1 => :bar, 2**32 => :baz
Query-mode Benchmark
The query_mode: :in_list
is slower for most queries and scales miserably with the number of bits.
Stay with the default query-mode. Only use :in_list if your edge-case shows better performance.
Testing With RSpec
To assert that a specific flag is a bitfield flag and has the active?
, active
, and active=
methods and behavior use the following matcher:
require 'bitfields/rspec'
describe User do
it { should have_a_bitfield :active }
end
TODO
- convenient named scope
User.with_bitfields(xxx: true, yyy: false)
Authors
- Hellekin O. Wolf
- John Wilkinson
- PeppyHeppy
- kmcbride
- Justin Aiken
- szTheory
- Reed G. Law
- Rael Gugelmin Cunha
- Alan Wong
- Andrew Bates
- Shirish Pampoorickal
- Sergey Kojin
Michael Grosser
michael@grosser.it
License: MIT