Railsで簡単にビットフィールドを扱う

洋服の色を複数選択できるWebインターフェースを考えてみます
以下のような画面です



色選択のインターフェース


色の種類は16種類とします
あなたならどのようにデータをもたせますか?


大げさに実装するとこんな感じでしょうか?
例


作る側としては非常に面倒な仕事になります
createの時に relationを作成して、updateの時には不要となった中間テーブルを削除したり、
未選択のマークをしたりしないといけません


ここまで大げさにやる必要が無いのであればビットフィールドを使ってみましょう


ビットフィールドとは?


int32 または int64の各ビットに対して情報を付与する方法です



情報10進数
ブラック1
グレー2
ブルー4
レッド8
ホワイト16

上記のようにした場合 服のカラー属性には
ブラックとホワイトの洋服のカラーの値には 1 + 16 = 17 の値が保存されます
同様にグレーとレッドであれば 2 + 8 = 10 の値が保存されます


ビットフィールドは何が嬉しいのか?


  • 1つのフィールドで複数の状態を管理できる
  • 検索を拘束に行える

服のカラー属性1つでint32(unsigned)の場合は 32状態を管理できます
int64(unsigned)の場合は 64状態です


またSQLを高速に発行することが出来ます
例えばブラックの服を検索するのには以下のクエリですみます


SELECT `cloths`.* FROM `cloths` WHERE ((cloths.colors & 1) = 1)

メリットが有る一方 ビット演算を駆使して情報管理をするため、
ActiveRecordなどで実装しようとすると 中々大変な仕事になるため
先ほどの案を採用するほうが楽になってしまいます


ようやく本題... Railsで簡単にビットフィールドを扱うプラグインbitfields


Railsにおいては bitfieldsというgemを利用することで簡単にこの機能を利用することが出来ます


Gemfileへの追加


以下をGemfileに追加し、bundleを実行してください


# Gemfile
gem 'bitfields'

bitfieldsを使ったカラーの管理


default: 0null: falseを指定しないと正しく動かなくなりますので注意しましょう


# migration file
class CreateCloths < ActiveRecord::Migration
def change
create_table :cloths do |t|
t.integer :color_bitfield, default: 0, null: false

t.timestamps null: false
end
end
end

16種類の色を定義した例です
(2の倍数の数値を入れていくのが非常に面倒です)


class Cloth < ActiveRecord::Base
include Bitfields
bitfield :color_bitfield, {
1 => :black, 2 => :gray, 4 => :white, 8 => :brown, 16 => :khaki, 32 => :beige,
64 => :red, 128 => :pink, 256 => :orange, 512 => :yellow, 1024 => :purple,
2_048 => :green, 4_096 => :navy, 8_192 => :blue, 16_384 => :gold, 32_768 => :silver
}
end

bitfields を使って Insert文を発行する


Cloth.create(black: true, pink: true, blue: true)
# INSERT INTO `cloths` (`color_bitfield`, `created_at`, `updated_at`) VALUES (8321, '2016-01-28 15:05:54', '2016-01-28 15:05:54')

1 + 128 + 8192 = 8321 を計算して勝手に設定してくれました


bitfields を使って 指定したカラーを取り出す


ブルーの洋服を取り出す


Cloth.blue.all
# SELECT `cloths`.* FROM `cloths` WHERE ((cloths.color_bitfield & 8192) = 8192)

ブルーでない洋服を取り出す


Cloth.not_blue.all
# SELECT `cloths`.* FROM `cloths` WHERE ((cloths.color_bitfield & 8192) = 0)

ブルーかつブラックの洋服を取り出す


Cloth.blue.black.all
# SELECT `cloths`.* FROM `cloths` WHERE ((cloths.color_bitfield & 8192) = 8192) AND ((cloths.color_bitfield & 1) = 1)

ただし上は効率が悪いので以下の方法もあります


Cloth.where(Cloth.bitfield_sql(blue: true, black: true)).all
# SELECT `cloths`.* FROM `cloths` WHERE ((cloths.color_bitfield & 8193) = 8193)

ブルーかつブラックでない洋服を取り出す


Cloth.where(Cloth.bitfield_sql(blue: true, black: false)).all
# SELECT `cloths`.* FROM `cloths` WHERE ((cloths.color_bitfield & 8193) = 8192)

モデルに追加される各種メソッド


cloth = Cloth.new(black: true, blue: true)
cloth.black? # => true
cloth.blue? # => true
cloth.red? # => false
cloth.color_bitfield # => 8193
cloth.bitfield_values(:color_bitfield)
=> {:black=>true, :gray=>false, :white => false ... }

他にも Update文で利用する set_bitfield_sql(blue: true)などもあります

欠点等


ブラック または ブルーの服を取り出すなどができない?
scopeにprefixをつけたりができない


必要に応じてforkして自分独自のカスタマイズまたはPullRequestを送りましょう

関連記事
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。