Store up to 64 multiple flags (bit array) in a single integer column with ActiveRecord. From a UI standpoint, it can be used as a multi-select checkbox storage.
Perfect solution to store multiple boolean values such as preferences, notification settings, achievement status, profile options, etc. in a single column.
- Single column to group multiple boolean values. You don't need to have many separate columns. You don't even need a migration when you add a new flag item to the list.
- Fast bitwise operations.
WHERE languages & 3 > 0is faster thanWHERE (english = true) OR (spanish = true) OR ...
If you want a simple enum column, take a look at EnumAccessor.
If you need to work with huge bit arrays, take a look at Bitwise.
classProfile < ActiveRecord::Baseflag:languages,[:english,:spanish,:chinese,:french,:japanese]end# {:english=>1, :spanish=>2, :chinese=>4, :french=>8, :japanese=>16 }# Instance methodsprofile.languages#=> #<ActiveFlag::Value: {:english, :japanese}>profile.languages.english?#=> trueprofile.languages.set?(:english)#=> trueprofile.languages.unset?(:english)#=> falseprofile.languages.set(:spanish)profile.languages.unset(:japanese)profile.languages.raw#=> 3profile.languages.to_a#=> [:english, :spanish]profile.languages=[:spanish,:japanese]# Direct assignment that works with forms# Class methodsProfile.languages.maps#=> {:english=>1, :spanish=>2, :chinese=>4, :french=>8, :japanese=>16 }Profile.languages.humans#=> {:english=>"English", :spanish=>"Spanish", :chinese=>"Chinese", :french=>"French", :japanese=>"Japanese"}Profile.languages.pairs#=> {"English"=>:english, "Spanish"=>:spanish, "Chinese"=>:chinese, "French"=>:french, "Japanese"=>:japanese}Profile.languages.to_array(3)#=> [:english, :spanish]# Scope methodsProfile.where_languages(:french,:spanish)#=> SELECT * FROM profiles WHERE languages & 10 > 0Profile.where_all_languages(:french,:spanish)#=> SELECT * FROM profiles WHERE languages & 10 = 10Profile.where_not_languages(:french,:spanish)#=> SELECT * FROM profiles WHERE languages & 10 = 0Profile.where_not_all_languages(:french,:spanish)#=> SELECT * FROM profiles WHERE languages & 10 < 10Profile.languages.set_all!(:chinese)#=> UPDATE "profiles" SET languages = COALESCE(languages, 0) | 4Profile.languages.unset_all!(:chinese)#=> UPDATE "profiles" SET languages = COALESCE(languages, 0) & ~4gem'active_flag'It is recommended to set 0 by default.
t.integer:languages,null: false,default: 0,limit: 8# ORadd_column:users,:languages,:integer,null: false,default: 0,limit: 8limit: 8 is only required if you need more than 32 flags.
For a querying purpose, use where_[column], where_all_[column],
where_not_[column] and where_not_all_[column] scopes.
Profile.where_languages(:french)#=> SELECT * FROM profiles WHERE languages & 8 > 0Also takes multiple values.
Profile.where_languages(:french,:spanish)#=> SELECT * FROM profiles WHERE languages & 10 > 0By default, it returns profiles that have either French or Spanish.
To get profiles that have both French and Spanish, use:
Profile.where_all_languages(:french,:spanish)#=> SELECT * FROM profiles WHERE languages & 10 = 10To get profiles that do not have either French or Spanish, use:
Profile.where_not_languages(:french,:spanish)#=> SELECT * FROM profiles WHERE languages & 10 = 0To get profiles that do not have both French and Spanish, use:
Profile.where_not_all_languages(:french,:spanish)#=> SELECT * FROM profiles WHERE languages & 10 < 10ActiveFlag supports i18n just as ActiveModel does.
For instance, create a Japanese translation in config/locales/ja.yml
ja:
active_flag:
profile:
languages:
english: 英語spanish: スペイン語chinese: 中国語french: フランス語japanese: 日本語and now to_human method returns a translated string.
I18n.locale=:japrofile.languages.to_human#=> ['英語', 'スペイン語']I18n.locale=:enprofile.languages.to_human#=> ['English', 'Spanish']Thanks to the translation support, forms just work as you would expect with the pairs convenience method.
# With FormBuilder=form_for(@profile)do |f|=f.collection_check_boxes:languages,Profile.languages.pairs# With SimpleForm=simple_form_for(@profile)do |f|=f.input:languages,as: :check_boxes,collection: Profile.languages.pairsThere are plenty of gems that share the same goal. However they have messy syntax than necessary in my opinion, and I wanted a better API to achieve that goal.
Also, ActiveFlag has one of the simplest code base that you can easily reason about or hack on.