2021-05-07

ActiveRecordでbitmaskを扱う

ActiveRecordでbitmaskを扱うgemを数年前に書きました。 alpaca-tc/active_record_bitmask

ぼちぼちデキはいいんですが、今まで宣伝し忘れて居たので紹介します。

bitmaskを扱えると何が嬉しいのか

bitmaskを利用すると、複数の値を1カラムで表現することができます。
実際の定義は下記のような形となります。

class User < ApplicationRecord
  # rolesはintのカラム
  #
  # 下記の例では{ administrator: 1, provider: 2, guest: 4 } にマッピングされる
  bitmask(roles: [:administrator, :provider, :guest])
end

そして、複数の値を格納しても、ビットマスクであれば1カラムの数値で表現されます。

user = User.new(roles: [:administrator, :guest])
user.save! #=> DBには 5 が記録される(administrator = 1, guest = 4なので 合計値は5)

user.roles #=> [:administrator, :guest]
user.roles?(:administrator) #=> true
user.roles?(:guest) #=> true
user.roles?(:provider) #=> false

既存gem(bitmask_attributes)との比較

すでにメンテナンスされていないですが、joelmoss/bitmask_attributesというgemがありました。

このgemも同様の機能を提供していたのですが、いくつか気になる点がありました。

  • コード読み込み時にカラム情報を取得するので、DBが存在していないと即エラーになる
  • ビットマスクのカラムへのクエリが数式で実行されるため、indexを利用できない

なにより、メンテナンスおらず利用には不安があったため、新しく作ることにしました。

indexを使うための工夫

bitmask_attributesでは、検索時に where("column & ? > 0", value) のようなクエリを発行していました。これでは計算が必要なのでindexを利用することはできません。

active_record_bitmaskは、これを解決するためにクエリを発行する前に、対象となる値を算出することにしました。
例えば、マッピングが{ administrator: 1, provider: 2, guest: 4 } であれば、administrator: 1を含む可能性のあるbitmask値は [1, 3, 5, 7] のいずれかになることが明らかです。
このように、あらかじめ算出した値をwhere句に渡すことで、インデックスを利用してbitmaskカラムを扱えるようにしました。

おわりに

利用シーンとしては、権限や機能制限のフラグなどの管理がやりやすいです。
後から新しい値を追加した時にも、わざわざカラムを追加せず済んでいいですよ。

ぜひお使いください。