IoTみたいな小さなアプリケーションで、sqliteを使うほどでも無いけどクエリは動いてほしい、みたいなことがあると思う。
こういう用途に対しては、golangで簡単なドキュメント指向データベースをつくって、pythonとかから呼び出せるようにしてやれば便利そうだ。というわけでこの機会にちょっと試作してみた。
基本方針
- goleveldbをバックエンドに使った適当なドキュメント指向DBっぽいもの
- Shared Libraryとして吐き出して他の言語から使えるようにする
データベースの実装
適当に実装を練った。 テスト用というかイメージをつかむためにつくったので、極めて軽くつくった実装
# これをドキュメントDBと言ってよいかどうかは悩ましい
package docdb
import (
"bytes"
"encoding/json"
"fmt"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
"reflect"
"strconv"
)
type Bucket struct {
parent *DB
name []byte
}
func (b *Bucket) Store(doc []byte) error {
val := map[string]interface{}{}
err := json.Unmarshal(doc, &val)
if err != nil {
return fmt.Errorf("invalid document type")
}
idval, ok := val["id"]
if !ok {
return fmt.Errorf("property 'id' is not found")
}
id, err := b.generateLevelDbID(idval)
if err != nil {
return err
}
err = b.parent.db.Put(id, doc, nil)
if err != nil {
return err
}
return nil
}
func (b *Bucket) getByStringID(id interface{}) ([]byte, error) {
lid, err := b.generateLevelDbID(id)
if err != nil {
return nil, err
}
dat, err := b.parent.db.Get(lid, nil)
if err != nil {
return nil, err
}
return dat, nil
}
func (b *Bucket) GetByQuery(query []byte) ([]byte, error) {
it := b.parent.db.NewIterator(util.BytesPrefix(b.name), nil)
defer it.Release()
queryobj := map[string]interface{}{}
err := json.Unmarshal(query, &queryobj)
if err != nil {
return nil, err
}
dbiterator:
for it.Next() {
obj := map[string]interface{}{}
err = json.Unmarshal(it.Value(), &obj)
if err != nil {
continue
}
for qk, qv := range queryobj {
if v, ok := obj[qk]; ok {
if !reflect.DeepEqual(qv, v) {
continue dbiterator
}
}
return it.Value(), nil
}
return nil, nil
}
func (b *Bucket) generateLevelDbID(id interface{}) ([]byte, error) {
idstr := ""
switch t := id.(type) {
case int:
idstr = strconv.Itoa(t)
case string:
idstr = t
default:
return nil, fmt.Errorf("invalid ID type")
}
return bytes.Join([][]byte{b.name, {0x2d}, []byte(idstr)}, nil), nil
}
type DB struct {
db *leveldb.DB
}
func OpenDB(path string) (*DB, error) {
bdb, err := leveldb.OpenFile(path, nil)
if err != nil {
return nil, err
}
return &DB{
db: bdb,
}, nil
}
func (db *DB) GetBucket(name string) *Bucket {
return &Bucket{
parent: db,
name: []byte(name),
}
}
func (db *DB) Close() error {
return db.db.Close()
}
Shared Libraryとして吐き出す
go 1.5からshared libraryを吐けるようになったので、このライブラリを他の言語からでも使ってやれる。
この時、パッケージはmainでなければならないらしい。//export
から始まるコメントでCから関数を使えるようにするのは普通のcgoと同様の仕組みっぽい。
package main
import (
"C"
"github.com/umisama/docdb/docdb"
"unsafe"
)
func main() {}
//export open
func open(path string, db unsafe.Pointer) int {
var err error
adb, err := docdb.OpenDB(path, true)
if err != nil {
return -1
}
db = unsafe.Pointer(adb)
return 0
}
//export get
func get(db unsafe.Pointer, bucket, query, data string) int {
adb := (*docdb.DB)(db)
data, err := adb.GetBucket(bucket).GetByQuery([]byte(query))
if err != nil {
return -1
}
return 0
}
//export put
func put(db unsafe.Pointer, bucket, data string) int {
adb := (*docdb.DB)(db)
err := adb.GetBucket(bucket).Store([]byte(data))
if err != nil {
return -1
}
return 0
}
これを、-build-mode=c-shared
を付けてビルドする。これでopen()、put()、get()の3つの関数を持ったlibraryを生成できる。
$ go build -buildmode=c-shared -o docdb.so
問題点
外部からこのshared libraryを呼び出すとき、Goの世界のstringは*charではなくて、GoStringという型になる。これはlibraryのビルド時に同時に生成されるhファイルで以下のように定義される。
typedef struct { char *p; GoInt n; } GoString;
これが(特にLL言語から)呼び出すときに面倒な手続きになる。これを書くくらいなら別にsqliteでも・・・とかちょっと思ってしまった。
解決策が無いか、今後検討していきたい。
追記
ライブラリ側で*C.char型で受ければ良いらしい。
func open(path *C.char, db unsafe.Pointer) int {
var err error
goPath := C.GoString(path)
adb, err := docdb.OpenDB(path, true)
~~~以下略~~