Deno v1.32でKVストアが実装されました
概要
先日、Deno v1.32がリリースされました。
このリリースでは、Deno本体にSQLiteベースのキーバリューストアが実装されています。
この記事では、このキーバリューストアの概要などについて紹介いたします。
基本
まず、このキーバリューストアの基本について紹介いたします。
サンプル
Deno.openKvというAPIが新しく追加されています。(このAPIの利用には--unstableの指定が必要です)
このAPIを呼ぶことで得られるDeno.Kvオブジェクトを使用してデータベースを操作します。
const kv = await Deno.openKv();
// キーの検索
const result = await kv.get(["key"]);
assert(result.key.length === 1);
assert(result.key[0] === "key");
assert(result.value == null);
assert(result.versionstamp == null);
// エントリの作成
await kv.set(["key"], "value");
const result2 = await kv.get(["key"]);
assert(result2.key.length === 1);
assert(result2.key[0] === "key");
assert(result2.value === "value");
assert(result2.versionstamp !== null);
// キーの削除
await kv.delete(["key"]);
// データベースを閉じます。
await kv.close();
キーについて
キーはDeno.KvKeyというデータ型として定義されています。
Deno.KvKeyの実体はDeno.KvKeyPartというデータ型の配列です。
Deno.KvKeyPartには以下のいずれかの値が許可されます。
Uint8Arraystringnumberbigintboolean
Denoがデータベースへキーと値のペアを保存する際は、Deno.KvKeyに含まれる各要素とそのデータ型をバイト列へエンコードして連結した値をデータベースのキーとして扱います。
mutation
Deno.Kvはデータベース上のキーを操作するためのメソッドを提供しており、例えば、以下のようなAPIがあります。
-
Deno.Kv.set- データベースにキーと値のペアを作成することができます。await kv.set(["key1"], "value"); -
Deno.Kv.delete- データベースから特定のキーを削除することができます。await kv.delete(["key"]);
これらの副作用を伴う操作をmutationと呼びます。
Deno.Kv.list
データベースに保存されているキーの一覧を検索するために、Deno.Kv.listというAPIが提供されています。
このAPIから返却されるDeno.KvListIteratorを使用することで、データベース上のキーを走査できます。
例えば、以下はキーがkeyから始まる全てのエントリを取得します。
const kv = await Deno.openKv();
await kv.set(["key", "1"], "foo");
await kv.set(["key", "2"], "bar");
await kv.set(["key", "3"], "baz");
await kv.set(["other_key"], "123");
const iter = kv.list({ prefix: ["key"] });
const entries = [];
for await (const entry of iter) {
entries.push(entry);
}
assert(entries.length === 3);
assert(JSON.stringify(entries[0].key) === `["key","1"]`);
assert(entries[0].value === "foo");
assert(JSON.stringify(entries[1].key) === `["key","2"]`);
assert(entries[1].value === "bar");
assert(JSON.stringify(entries[2].key) === `["key","3"]`);
assert(entries[2].value === "baz");
Deno.Kv.listでは様々なオプションが提供されており、それにより挙動を細かく調整できます。
| オプション | 説明 |
|---|---|
limit |
取得するキーの最大件数を指定できます |
reverse |
trueを指定すると、通常とは逆の順番でキーを走査できます |
cursor |
特定のキーから走査を開始したい場合に指定できます。 このオプションに指定する値は Deno.KvListIterator.cursorから取得できます。 |
Deno.Kv.atomic
Deno.Kvに対して複数のmutationをアトミックに実行するために、Deno.Kv.atomicというAPIが提供されています。
このAPIからはDeno.AtomicOperationが返却されます。
Deno.AtomicOperationを介して行った操作は、明示的にDeno.AtomicOperation.commitメソッドを呼ぶまではデータベースには反映されません。
const kv = await Deno.openKv();
await kv.set(["key"], "foo");
const result = await kv.get(["key"]);
const ok = await kv.atomic()
.check({ key: ["key"], versionstamp: result.versionstamp })
.set(["key"], "bar")
.commit();
// Deno.AtomicOperationを介したmutationの実行は原子性が保証されており、
// いずれかのmutationに失敗した際は、すべてのmutationの実行が取り消されます。
assert(ok); // コミットに成功した際はtrueが返却されます。
const result2 = await kv.get(["key"]);
assert(result2.value === "bar");
await kv.close();
Deno.AtomicOperation.check
上記の例では、Deno.AtomicOperation.checkというAPIが利用されていました。
Deno.Kv.getなどの結果に含まれるversionstampプロパティは、このAPIを使用する際に活用することができます。
このAPIの引数には、以下のようなDeno.AtomicCheckオブジェクトを指定します。
const atomicOps = kv.atomic();
atomicOps.check({
key: ["key"], // mutationの実行前に有効性を検証したいキー
versionstamp: result.versionstamp, // このキーにおいて期待されるバージョン
});
// このAPIは複数回呼び出すことができます
atomicOps.check({
key: ["other_key"],
versionstamp: result2.versionstamp
});
Deno.AtomicOperation.commitを呼んだ際は、mutationを実行する前にまずcheckで指定されたversionstampの値をもとに、キーの有効性が判断されます。
もし一つでもversionstampが異なるキーが存在すれば、mutationは実行されずキャンセルされます。
これにより、例えば、楽観的ロックの実装などに活用することが期待されているようです。
Deno.AtomicOperation.mutate
このAPIはDeno.Kvでサポートされる任意のmutationを実行したい場合に利用できます。
例えば、"sum"mutationを実行することで、特定のキーの値をインクリメントできます。
await kv.set(["key"], new Deno.KvU64(1n));
const result = await kv.get(["key"]);
const ok = await kv.atomic()
.check({ key: ["key"], versionstamp: result.versionstamp })
.mutate({ type: "sum", key: ["key"], value: new Deno.KvU64(2n) })
.commit();
assert(ok);
const result2 = await kv.get(["key"]);
console.info(result2);
assert(result2.value instanceof Deno.KvU64);
assert(result2.value.value === 3n);
これ以外にも様々なmutationがサポートされており、詳しくはDeno.KvMutationを参照いただければと思います。
consistencyオプションについて
Deno.Kv.getなどのAPIの型定義ではconsistencyというオプションが定義されています
このオプションは以下のいずれかの値を指定する想定のようです:
| オプション | 説明 |
|---|---|
"strong" |
強整合性を保証します (デフォルトの挙動) |
"eventual" |
結果整合性を保証します |
今のところ、このオプションについては型定義の提供のみにとどまっているようで、まだ実装はされていないようです。
これは完全なる予想ではありますが、例えば、将来的にDeno DeployにDeno.openKvが実装されるようなことがあれば、このオプションが本格的に意味をなしてくるのではないかと予測しています。
補足
ファイルの保存先について
データベースファイルの保存先は、Deno.openKv()に与えた引数によって決定されます。
引数を指定しなかった場合は、$DENO_DIR/location_data/<SHA256ハッシュ>/kv.sqlite3に保存されます。 (実際の保存先はdeno infoコマンドで出力されるOrigin storageから確認できます)
SHA256ハッシュは以下のようにして計算されます。(localStorageなどと同様の規則で計算されます)
-
--locationオプションが指定されている場合は、そのOriginをハッシュ化します。 -
deno.json(c)が存在する場合は、その設定ファイルのspecifier (例:file:///path/to/dir/deno.json)をハッシュ化します。 - 上記いずれにも該当しない場合は、エントリポイントとして指定されたモジュールのspecifier(例:
file:///path/to/dir/main.ts)をハッシュ化します。
Deno.openKv()にパスを指定した場合は、その場所へファイルが保存されます。(この場合、--allow-readと--allow-writeパーミッションが必要です)
// ./kv.sqliteにデータベースを保存します
const kv = await Deno.openKv("kv.sqlite");
また、Deno.Kvによって作成されるデータベースの実体は通常のSQLiteデータベースであるため、各種GUIツールなどを使用して内容を閲覧することもできます。
# 例)
$ sqlitebrowser $DENO_DIR/location_data/<ハッシュ>/kv.sqlite3
また、":memory:"を指定することで、DBの内容をファイルには永続化せず、インメモリで管理することもできます。 (この場合は、--allow-readと--allow-writeの指定は不要になります。)
おわりに
以上、Deno v1.32で実装されたキーバリューストアに関する紹介でした。
まだDenoの公式からは宣伝や紹介などは行われていないものの、比較的重要な機能なのではないかと思ったため、紹介させていただきました。
今後の動向についてはまだわからないものの、もしこのAPIがDeno Deployなどに実装されれば、かなり利便性が上がるのではないかと感じているため、個人的には非常に期待をしております。
Discussion
ご紹介ありがとうございます。さっそくトライしてみたのですが、大きめのstringを保存しようとすると
TypeError: value too large (max 65536 bytes)と出てしまうので、sqliteの仕様そのままというわけではなさそうですね。他にも試してみる方がいるかもしれないのでコメントしておきます。Nice, amazing introduction. ありがとう!