🦕

Deno v1.32でKVストアが実装されました

2023/03/26に公開
2

概要

先日、Deno v1.32がリリースされました。

https://deno.com/blog/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には以下のいずれかの値が許可されます。

  1. Uint8Array
  2. string
  3. number
  4. bigint
  5. boolean

Denoがデータベースへキーと値のペアを保存する際は、Deno.KvKeyに含まれる各要素とそのデータ型をバイト列へエンコードして連結した値をデータベースのキーとして扱います。


ext/kv/codec.rs#L36-L65

mutation

Deno.Kvはデータベース上のキーを操作するためのメソッドを提供しており、例えば、以下のようなAPIがあります。

  • Deno.Kv.set - データベースにキーと値のペアを作成することができます。

    await kv.set(["key1"], "value");
    
  • Deno.Kv.delete - データベースから特定のキーを削除することができます。

    await kv.delete(["key"]);
    

これらの副作用を伴う操作をmutationと呼びます。


ext/kv/lib.rs#L274-L297

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などと同様の規則で計算されます)

  1. --locationオプションが指定されている場合は、そのOriginをハッシュ化します。
  2. deno.json(c)が存在する場合は、その設定ファイルのspecifier (例: file:///path/to/dir/deno.json)をハッシュ化します。
  3. 上記いずれにも該当しない場合は、エントリポイントとして指定されたモジュールの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

kyoheiukyoheiu

ご紹介ありがとうございます。さっそくトライしてみたのですが、大きめのstringを保存しようとするとTypeError: value too large (max 65536 bytes)と出てしまうので、sqliteの仕様そのままというわけではなさそうですね。他にも試してみる方がいるかもしれないのでコメントしておきます。