📙

buntdbを使ってみた

2021/03/19に公開

buntdb とは

tidwall/buntdbは Pure Go で書かれた KVS ライブラリ。
インメモリで処理を行うが、ディスクに永続化することもできる。またトランザクションや Index がある。
Go からは mattn/go-sqlite3を使えば SQLite が使えるが、SQLite の利用には cgo が必要なので、環境によってはややビルド方法などで気を使う必要がある。
そのため Pure Go で使える、軽量に組み込める DB ライブラリがほしかった。
buntdb は KVS なので SQLite ほど柔軟にクエリや集計ができるわけではないが、用途によっては使えるかと思った。

シンプルな使い方

https://play.golang.org/p/wSHTfPSmmHp

package main

import (
	"fmt"
	"log"

	"github.com/tidwall/buntdb"
)

func main() {
	db, err := buntdb.Open(":memory:")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 読み書きができるトランザクションを生成
	db.Update(func(tx *buntdb.Tx) error {
		// レコードの登録
		tx.Set("key3", "value3", nil)
		tx.Set("key1", "value1", nil)
		tx.Set("key2", "value2", nil)
		return nil // エラーが返らない場合コミットされる
	})

	// 読み込みのみのトランザクションを生成
	db.View(func(tx *buntdb.Tx) error {
		// 全レコードを昇順でイテレート
		tx.Ascend("", func(key, value string) bool {
			fmt.Println(key, value)
			return true
		})
		return nil
	})
}

結果

key1 value1
key2 value2
key3 value3
  • トランザクションは、読み書き, 読み込みのみ, どちらの場合も関数として表現する
  • Key も Value も string 型

Index を使う

https://play.golang.org/p/tchBZ89nFwy

package main

import (
	"fmt"
	"log"

	"github.com/tidwall/buntdb"
)

func main() {
	db, err := buntdb.Open(":memory:")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// keyに`key:`というprefixがつくレコードを対象に、valueを文字列としてソートする`indexstr`という名前のインデックスを生成
	db.CreateIndex("indexstr", "key:*", buntdb.IndexString)
	// keyに`key:`というprefixがつくレコードを対象に、valueを数値としてソートする`indexint`という名前のインデックスを生成
	db.CreateIndex("indexint", "key:*", buntdb.IndexInt)

	db.Update(func(tx *buntdb.Tx) error {
		tx.Set("key:1", "13", nil)
		tx.Set("key:2", "2", nil)
		tx.Set("key:3", "9", nil)
		return nil // エラーが起きていなければ自動でコミットされる
	})

	db.View(func(tx *buntdb.Tx) error {
		fmt.Println("iterate over indexstr")
		tx.Ascend("indexstr", func(key, value string) bool {
			fmt.Println(key, value)
			return true
		})

		fmt.Println("iterate over indexint")
		tx.Ascend("indexint", func(key, value string) bool {
			fmt.Println(key, value)
			return true
		})
		return nil
	})
}

結果

iterate over indexstr
key:1 13
key:2 2
key:3 9
iterate over indexint
key:2 2
key:3 9
key:1 13

Multi Value Index を使う

https://play.golang.org/p/eEMPdWVoy6X

package main

import (
	"fmt"
	"log"

	"github.com/tidwall/buntdb"
)

func main() {
	db, err := buntdb.Open(":memory:")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
	// 全レコードを対象に、valueをJSONとみなしてi1, i2の値の順にソートする`index`という名前のインデックスを生成
	db.CreateIndex("index", "*", buntdb.IndexJSON("i1"), buntdb.IndexJSON("i2"))

	db.Update(func(tx *buntdb.Tx) error {
		var err error
		records := []struct {
			Key   string
			Value string
		}{
			// 値が文字列
			{Key: "s12", Value: `{"i1":"1", "i2":"2"}`},
			{Key: "s21", Value: `{"i1":"2", "i2":"1"}`},
			{Key: "s22", Value: `{"i1":"2", "i2":"2"}`},
			{Key: "s322", Value: `{"i1":"3", "i2":"22"}`},
			{Key: "s32", Value: `{"i1":"3", "i2":"2"}`},
			{Key: "s11", Value: `{"i1":"1", "i2":"1"}`},
			{Key: "s31", Value: `{"i1":"3", "i2":"1"}`},
			{Key: "s33", Value: `{"i1":"3", "i2":"3"}`},

			// 値が数値型
			{Key: "n12", Value: `{"i1":1, "i2":2}`},
			{Key: "n21", Value: `{"i1":2, "i2":1}`},
			{Key: "n22", Value: `{"i1":2, "i2":2}`},
			{Key: "n322", Value: `{"i1":3, "i2":22}`},
			{Key: "n32", Value: `{"i1":3, "i2":2}`},
			{Key: "n11", Value: `{"i1":1, "i2":1}`},
			{Key: "n31", Value: `{"i1":3, "i2":1}`},
			{Key: "n33", Value: `{"i1":3, "i2":3}`},
		}
		for _, record := range records {
			_, _, err = tx.Set(record.Key, record.Value, nil)
		}
		return err
	})

	db.View(func(tx *buntdb.Tx) error {
		// indexを昇順にイテレート
		fmt.Println("iterate over index")
		tx.Ascend("index", func(key, value string) bool {
			fmt.Println(key, value)
			return true
		})

		// indexの値が `{"i1":3, "i2":1}` と同じレコードをイテレート
		fmt.Println("iterate over index equal to n31")
		tx.AscendEqual("index", `{"i1":3, "i2":1}`, func(key, value string) bool {
			fmt.Println(key, value)
			return true
		})

		// indexの値が `{"i1":3, "i2":1}` より大きいレコードをイテレート
		fmt.Println("iterate over index greater or equal to n31")
		tx.AscendGreaterOrEqual("index", `{"i1":3, "i2":1}`, func(key, value string) bool {
			fmt.Println(key, value)
			return true
		})

		// i1 = 3のレコードをi2の値で昇順にイテレート
		fmt.Println("iterate over index where i1 = 3")
		tx.AscendRange("index", `{"i1":3, "i2":0}`, `{"i1":4, "i2":0}`, func(key, value string) bool {
			fmt.Println(key, value)
			return true
		})

		return nil
	})
}

結果

iterate over index
n11 {"i1":1, "i2":1}
n12 {"i1":1, "i2":2}
n21 {"i1":2, "i2":1}
n22 {"i1":2, "i2":2}
n31 {"i1":3, "i2":1}
n32 {"i1":3, "i2":2}
n33 {"i1":3, "i2":3}
n322 {"i1":3, "i2":22}
s11 {"i1":"1", "i2":"1"}
s12 {"i1":"1", "i2":"2"}
s21 {"i1":"2", "i2":"1"}
s22 {"i1":"2", "i2":"2"}
s31 {"i1":"3", "i2":"1"}
s32 {"i1":"3", "i2":"2"}
s322 {"i1":"3", "i2":"22"}
s33 {"i1":"3", "i2":"3"}
iterate over index equal to n31
n31 {"i1":3, "i2":1}
iterate over index greater or equal to n31
n31 {"i1":3, "i2":1}
n32 {"i1":3, "i2":2}
n33 {"i1":3, "i2":3}
n322 {"i1":3, "i2":22}
s11 {"i1":"1", "i2":"1"}
s12 {"i1":"1", "i2":"2"}
s21 {"i1":"2", "i2":"1"}
s22 {"i1":"2", "i2":"2"}
s31 {"i1":"3", "i2":"1"}
s32 {"i1":"3", "i2":"2"}
s322 {"i1":"3", "i2":"22"}
s33 {"i1":"3", "i2":"3"}
iterate over index where i1 = 3
n31 {"i1":3, "i2":1}
n32 {"i1":3, "i2":2}
n33 {"i1":3, "i2":3}
n322 {"i1":3, "i2":22}
  • buntdb.IndexJSON()を使うと JSON の特定のフィールドに対しての Index を作れる
  • buntdb.CreateIndex()の引数に複数の Index 関数を設定できる
  • buntdb.IndexJSON()の順は JSON 中の型による。文字列型であれば文字列として、数値型であれば数値として並ぶ

Discussion