導入・背景
たまたま仕事でビットフラグを用いたデータ登録を行う機会がありました。
Rubyでは、active_flagなるビットフラグを用いたデータ登録をサポートしてくれるgemがあり、それによって簡単にデータ登録ができるようになります。
「JSでも同じようなライブラリがあるかなー」と探してみたのですが、あまり無いっぽいのでロジックだけ作ってみました。(有名なライブラリがあるときはコメントしてくれると嬉しいです。)
もしよかったら、見てみてください!
そもそも、ビットフラグってなに?
ビット単位のフラグは変数の集合で、通常は単純な数値であり、メソッドやその他のコード構造の特定の使用法や機能を有効にしたり無効にしたりするのに使われる。ビットレベルで動作するため、迅速かつ効率的にこれを行うことができる。同じグループ内の関連フラグには、一般に、1 つの値(16 進数など)の異なるビット位置を表す相補的な値が与えられ、複数のフラグ設定を 1 つの値で表すことができます。
https://developer.mozilla.org/en-US/docs/Glossary/Bitwise_flags
よくわかんないですが、簡単にいうと
整数の各ビット(0または1)を使って、複数の状態やフラグを一つの数値で表現する仕組み
です。
例えば、「好きなサメは?」という選択肢(複数回答可)があったとします。
選択肢としては
- シュモクザメ
- ジンベイザメ
- ウバザメ
- ホホジロザメ
- レモンザメ
- ツマグロ
- ダルマザメ
- ウバザメ
- シロワニ(一番推し)
があるとします。
この時、回答内容を保存する方法としては以下が考えられると思います。
- 中間テーブルを設けて選択された数だけレコードを作成する
- JSON型として保存する
- 1つのカラムにカンマ区切りで登録する
- ...etc
しかし、
- 選択肢の回答だけでテーブルを作成するのもやり過ぎな感じがしますし、
- JSON型で登録する場合もSQLをゴリゴリに書かないといけないし、
- カンマ区切りで登録するのはアンチパターンだし...
ということで、ビットフラグの出番です!
次のようにビット(2進数)毎に選択肢を定義した場合を考えてみます。
ビット値 | 10進数 | サメ名 |
---|---|---|
00000001 | 1 | シュモクザメ |
00000010 | 2 | ジンベイザメ |
00000100 | 4 | ウバザメ |
00001000 | 8 | ホホジロザメ |
00010000 | 16 | レモンザメ |
00100000 | 32 | ツマグロ |
01000000 | 64 | ダルマザメ |
10000000 | 128 | シロワニ |
計算方法としては、選択されたサメの各10進数の和を求めることで、1つのレコードに複数の選択情報を格納することができます。
例えば、以下のようなイメージです。
// 「シュモクザメ」「ホホジロザメ」が選択された場合
1:シュモクザメ + 8:ホホジロザメ = 9
// 「ジンベイザメ」「ツマグロ」「シロワニ」が選択された場合
2:ジンベイザメ + 8:ツマグロ + 128:シロワニ = 138
→ 選択肢に対応する10進数の和がDBに登録される!
大体イメージが掴めてきたでしょうか?
選択肢を1つの数値に圧縮して管理することで、
「複数の選択肢を効率的に管理し、ストレージやパフォーマンスの観点で優れている」らしいです。
しかし、登録データが10進数の値だけなので、
「パッと見でどんな値が登録されているか分かりづらい」といったデメリットもあるようです。
Enumとかもありますし、適切に使い分けしたほうがいいってことですかね。。。?
(このあたりの選定基準はあまり理解できていないので、コメントしてくれると嬉しいです!)
Rubyにはなってしまいますが、なぜビットフラグを選定するのかについてわかりやすい記事があったため、ぜひ参考にしてみてください!
↓
ビットフラグを使ったデータ登録
いよいよ、Prismaでビットフラグを用いたデータ登録の方法を解説したいと思います。
(※Prismaを使用する理由としては、個人開発で使っているからという安直な理由です。。)
前提:Prismaスキーマの定義
Prismaでは、Int
型を使ってビットフラグを保存します。
以下は、flags
フィールドを持つUser
モデルの例です。
model User {
id Int @id @default(autoincrement())
name String
flags Int @default(0)
}
このflags
フィールドをビットフラグとして使用します。
TypeScriptでのビットフラグ管理
TypeScriptを用いて、ビットフラグのデコードとエンコードを行うユーティリティ関数を作成します。
①フラグマップ及び型の作成
まず、2進数を10進数に変換した値をキーに持つサメの名前が記述されたオブジェクトを作成します。
type FlagMap = Record<number, string>
const FLAGS: FlagMap = {
1: 'シュモクザメ',
2: 'ジンベイザメ',
4: 'ウバザメ',
8: 'ホホジロザメ',
16: 'レモンザメ',
32: 'ツマグロ',
64: 'ダルマザメ',
128: 'シロワニ',
}
②ビットフラグのデコード
次に、数値のビットフラグから対応するフラグ名を取得する関数を定義します。
const decodeFlags = (value: number): string[] =>
Object.entries(FLAGS)
.filter(([key]) => (value & Number(key)) === Number(key))
.map(([, label]) => label)
// 使用例
console.log(decodeFlags(3)) // ['シュモクザメ', 'ジンベイザメ']
console.log(decodeFlags(5)) // ['シュモクザメ', 'ウバザメ']
console.log(decodeFlags(128)) // ['シロワニ']
解説
Object.entries
でキーとバリューを値に持つ配列を生成します。
console.log(Object.entries(FLAGS))
// [['1', 'シュモクザメ'], ['2', 'ジンベイザメ'], ['4', 'ウバザメ'], ...]
次にビット演算を用いて、filter
でvalue
に設定されているビットを抽出しています。
value & Number(key)
は、ビット演算のAND(&)を用いて計算されており、
key
とvalue
の該当するビットが両方「1」の場合のみ、対応するビットが「1」になります。
その結果がNumber(key)
と等しい場合、そのビットがvalue
に含まれていることを意味します。
以下のようなイメージですね!
↓
value = 3(2進数で 11)、key = 1(2進数で 01)
→ 3 & 1 → 01(1)となり、条件が真になります。
value = 3(2進数で 11)、key = 4(2進数で 100)
→ 3 & 4 → 000(0)となり、条件が偽になります。
この記事がとてもわかり易かったので、ぜひ参考にしてください!
↓
最後にmap
によって、対応するフラグ名だけを配列として返されます。
これにより、ビットフラグを簡単に人間が読める形式に変換できます。
③ビットフラグのエンコード
次に、フラグ名の配列からビットフラグ値を生成する関数を定義します。
const encodeFlags = (labels: string[]): number =>
Object.entries(FLAGS)
.filter(([, label]) => labels.includes(label))
.reduce((acc, [key]) => acc | Number(key), 0)
// 使用例
console.log(encodeFlags(['シュモクザメ', 'ジンベイザメ'])) // 3
console.log(encodeFlags(['シュモクザメ', 'ウバザメ'])) // 5
console.log(encodeFlags(['シロワニ'])) // 128
解説
デコード時と同様にObject.entries
でキーとバリューを値に持つ配列を生成します。
console.log(Object.entries(FLAGS))
// [['1', 'シュモクザメ'], ['2', 'ジンベイザメ'], ['4', 'ウバザメ'], ...]
次にfilter
によって、引数に含まれるフラグ名を抽出します。
const encodeFlags = (labels: string[]): number =>
Object.entries(FLAGS)
.filter(([, label]) => labels.includes(label))
console.log(encodeFlags(['シュモクザメ', 'piyo'])) // [[1, "シュモクザメ"]]
console.log(encodeFlags(['hoge', 'ウバザメ'])) // [[4, "ウバザメ"]]
console.log(encodeFlags(['レモンザメ', 'シロワニ'])) // [[16, "レモンザメ"], [128, "シロワニ"]]
最後にreduce
によって、ビット値(キー)をOR演算(|
)で合成して求めます。
これにより、複数のフラグ(選択肢)を簡単に数値に変換できます。
Prismaとビットフラグの連携
①登録時にビットフラグをエンコード
Prismaでデータを登録する際に、フラグ名の配列をエンコードして保存します。
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function createUser(name: string, flags: string[]) {
const flagsValue = encodeFlags(flags) // ← ここでエンコード(ビットフラグに変換)している!
await prisma.user.create({
data: { name, flags: flagsValue },
})
}
createUser('鮫島鱶太郎', ['シュモクザメ', 'ダルマザメ'])
// 登録値としては以下のようなイメージ
// id: 1, name: '鮫島鱶太郎', flags: 65
②取得時にビットフラグをデコード
データを取得する際に、ビットフラグをデコードします。
async function getUser(id: number) {
const user = await prisma.user.findUnique({ where: { id } })
return {
...user,
flags: decodeFlags(user.flags), // ← ここでデコード(人間に読める値に変換)している!
}
}
const exmapleUser = await getUser(1)
console.log(exmapleUser)
// id: 1, name: '鮫島鱶太郎', flags: ['シュモクザメ', 'ダルマザメ']
ちょっと脱線:フラグマップを生成する関数
「①フラグマップ及び型の作成」にて、
type FlagMap = Record<number, string>
const FLAGS: FlagMap = {
1: 'シュモクザメ',
2: 'ジンベイザメ',
4: 'ウバザメ',
8: 'ホホジロザメ',
16: 'レモンザメ',
32: 'ツマグロ',
64: 'ダルマザメ',
128: 'シロワニ',
}
のような感じでフラグマップを作成していたと思います。
フラグの種類がたくさん存在する場合(例えば「クジラ」「イルカ」など)、毎回同じようなフラグマップを作成するのは面倒なので、文字列が格納されている配列からフラグマップを生成する共通関数を作ってみました。
const generateFlags = (keys: string[]): FlagMap => {
return keys.reduce((acc, key, index) => {
const bitValue = 1 << index // 2^index
acc[bitValue] = key
return acc
}, {} as FlagMap)
}
const sharks = ['ウバザメ', 'ホホジロザメ', 'シロワニ']
const sharkFlags = generateFlags(sharks)
console.log(sharkFlags)
// {1: 'ウバザメ', 2: 'ホホジロザメ', 4: 'シロワニ'}
解説
reduce
を用いて、初期値{}
からFlagMap型
の計算結果を蓄積させるようにしています。
bitValue
では2進数のビット値を生成しています。
左ビットシフトっていうらしいです。
index = 0 の場合: 1 << 0 = 1(2進数で 001)
index = 1 の場合: 1 << 1 = 2(2進数で 010)
index = 2 の場合: 1 << 2 = 4(2進数で 100)
あとは取得したビット値をキー名、引数の文字列key
をバリューの値に設定することでフラグマップが生成されます。
acc[bitValue] = key
// 生成イメージ
// {}[1] = 'ウバザメ'
// {1: 'ウバザメ'}[2] = 'ホホジロザメ'
// {1: 'ウバザメ', 2: 'ホホジロザメ'}[4] = 'シロワニ'
// → {1: 'ウバザメ', 2: 'ホホジロザメ', 4: 'シロワニ'}
まとめ
今回はPrismaを用いて、エンコード・デコードによってビットフラグを簡単に扱える仕組みを構築してみました。
ビットフラグは、効率的にフラグ(冒頭では選択肢)を管理できる手法です。
私自身、普段Webアプリケーションの開発をしていますが、あまり目にする機会はなかったです。。。
実務での採用はケースバイケースだと思いますが、ビットフラグに関する知見を深めることが出来て良かったです。
(あまり使われていないのかな?そのあたりの知見がなく、わかる方がいらっしゃったらコメントしてくれると嬉しいです!)
また、シンプルなフラグ管理が必要な場合はenum
も有効な選択肢なのかなと思います。
enum
の設定方法については、別の記事で解説できればと思っています。
最後まで読んでいただき、本当にありがとうございました!
最後に
他にもいろいろな記事を投稿しているので、もしよかったら見てみてください!
ではでは!!