DenoにはDeno KVと呼ばれるデータをディスク上に永続的に保持する組み込みのキーバリューストアがあります。これによってDenoではサービスとシステムの再起動をまたいだデータの保存とアクセスが可能となります。
この機能はこれまでClosed Beta版として公開されていましたが、2023年9月6日にOpen Beta版として公開されました。
この記事ではそんなOpenされたばかりのDeno KVについて紹介します。
Open Beta版が始まったばかりで実験中の機能です。バージョンアップによって内容が変更される可能性があるので気をつけてください。
特徴
簡単なセットアップ
ローカル環境であってもDeno Deploy上であってもDeno KVは以下のように手間なく利用可能です。
await Deno.openKv();
Deno Deploy以外の環境からDeno Deploy上のリソースを使いたい場合はurlを入力する必要があります。
await Deno.openKv("https://api.deno.com/databases/XXX/connect");
JavaScript用のデータベース
Deno KVはJavaScriptもしくはTypeScriptで利用すること前提で作られているので、文字列や数値のような単調な値の他にもMap
やundefined
、Date
のようなJavaScriptで扱えるオブジェクトをそのまま値として使うことができます。
さらに、APIもPromiseベースで直感的に実装されているのでデータの操作が容易です。
undefined;
null;
true;
false;
42;
-42.5;
42n;
"hello";
new Uint8Array([1, 2, 3]);
[1, 2, 3];
{ a: 1, b: 2, c: 3 };
new Map([["a", 1], ["b", 2], ["c", 3]]);
new Set([1, 2, 3]);
new Date("2023-04-23");
/abc/;
const a = {};
const b = { a };
a.b = b;
// Denoがサポートするbigintとして表される64ビットの符号なし整数
new Deno.KvU64(42n);
簡単に拡張可能
Deno KVはFoundationDB上に構築されており、毎秒数百万のオペレーションを処理することができます。エンタープライズへの移行もゼロコンフィグ、ゼロプロビジョニング、ゼロオーケストレーションで行えます。
ACIDトランザクション
Deno KVではデータの一貫性、整合性、耐久性を確保した一連の操作にトランザクションを貼れます。
一連の操作が成功した場合はデータを永続化し、失敗した場合はロールバックされるのでデータが常に正確に保ちます。
await kv.atomic()
.check(senderRes)
.check(receiverRes)
.set(senderKey, newSenderBalance)
.set(receiverKey, newReceiverBalance)
.commit();
読み取りの精度
データの読み取りは通常プライマリーリージョンから最新のデータを読み取ったものを返します。データの読み取りに強い整合性を求めない場合に最も近いリージョンで読み取った最後の結果を返すように指定できます。
await kv.get(key, { consistency: "eventual" });
使ってみる
現在Deno KVの利用には、実行時に--unstable
フラグを設ける必要があります。実機で試す時は気をつけてください。
deno run --unstable --allow-net main.ts
構造
キーはKvKey
という型として実装されています。KvKey
はreadonly KvKeyPart[]
という型になっています。さらにKvKeyPart
はUint8Array
、string
、number
、bigint
、boolean
の5つの型のユニオンです。
つまり、['hello', 'world', 123]
のように一連のシーケンスでキーを表します。
バリューは特徴でも紹介しましたが、JavaScriptのオブジェクトも含んださまざまな値を扱えます。
undefined
null
boolean
number
string
bigint
Uint8Array
Array
Object
Map
Set
Date
RegExp
Deno.KvU64
値の循環参照もサポートされていて、かなり自由度が高いです。
Deno KVを開く
全ての操作はDeno.Kv
APIを介して行います。このAPIはDeno.openKv
関数をを呼び出すことで利用できます。
const kv = await Deno.openKv();
openKv
関数の第一引数には接続先を与えられます。Denoをデプロイする先がDeno Deployではない、もしくは異なるプロジェクトのDeno KVを利用する場合はパスを与えて接続先をカスタマイズします。
操作
主要な操作について紹介します。
get
get
は指定したキーに対応するバリューとバージョンを返します。
kv
からget
を呼び出して型引数でバリューの型を決めます。
await kv.get<string>(['hello', 'deno']);
get
の第2引数にはoptions
を渡せます。options
は現在consistency
をキーとするオブジェクトを渡すことができ、strong
とeventual
で取得する値の精度を決めます。デフォルトではstrong
となっており、eventual
にすると一番近いリージョンから取得した値を返します。
await kv.get<string>(
['hello', 'deno'],
{
consistency: 'eventual',
},
);
得られる型はPromise<KvEntryMaybe>
となっています。これは以下のように定義されています。
type KvEntry<T> = { key: KvKey; value: T; versionstamp: string };
type KvEntryMaybe<T> = KvEntry<T> | {
key: KvKey;
value: null;
versionstamp: null;
};
key
には指定したキーを、value
には取得した値のバリューを指定した型T
を、versionstamp
にはバージョン情報をstring
で返しています。値がなかった場合はvalue
とversionstamp
にnull
を詰めて返します。
詳細は紹介しませんが、getMany
によって複数のキーに対応する複数の値を取得することも可能です。
await kv.getMany<[string, string, string]>([
['hello', 'node'],
['hello', 'deno'],
['hello', 'ts'],
]);
list
list
は指定したセレクターに一致した値を返します。KvListIterator
というイテレーターとして実装されており、イテレータからは先ほどget
で見たKvEntry
が渡されます。
イテレーターから取り出す値はキーを元に定められた順番で規則正しく並んでいます。
まずは大雑把に型を元にUint8Array
、string
、number
、bigint
、boolean
の順番で並べられます。そして型の中でもそれぞれに定義された順番で並べられます。string
だとUTF-8エンコーディングのバイト順に並べられます。
kv.list<string>({ prefix: ["hello"] });
第一引数にはKvListSelector
で表現されるセレクターを渡します。
type KvListSelector =
| { prefix: KvKey }
| { prefix: KvKey; start: KvKey }
| { prefix: KvKey; end: KvKey }
| { start: KvKey; end: KvKey };
prefix
はそれと完全に一致したキーで始まる値を取り出します。start
やend
はキーの範囲を指定します。
例えばKvListSelector
を{ start: ['hello', 'a'], end: ['hello', 'e'] }
のようにした場合を考えます。この時はキーが['hello', 'deno']
の値が取り出されます。['hello', 'node']
や['hello', 'ts']
はnode
やts
の箇所で範囲外となるので取り出されないです。
end
に指定したキーは含まれないことに注意してください。end
が['hello', 'd']
の場合は['hello', 'deno']
は引っかかりません。
そして任意で第2引数をKvListOptions
として渡せます。
type KvListOptions = {
limit?: number;
cursor?: string;
reverse?: boolean;
consistency?: KvConsistencyLevel;
batchSize?: number;
}
limit
は名前の通り最大の取得件数を指定します。cursor
は取得位置を指定します。reverse
は逆順で返すように指定します。consistency
はget
と同じです。
batchSize
は裏側でデータを取得する時のサイズを指定します。デフォルトでは500になっています。直接は見えない設定ですがこの取得単位でデータの整合性が担保され、データ間の整合性は担保されないことに気をつけてください。
返ってきたイテレーターは(当然ですが)以下のようにループさせて取得します。
const iter = kv.list<string>({ prefix: ["hello"] });
for await (const res of iter) console.log(res.value);
set
set
は値の作成または更新を行います。
await kv.set(["users", "alex"], "alex");
キーを第1引数に、バリューを第2引数に渡します。
第3引数には任意で期限を渡せます。オブジェクトでexpireIn
をキーとしてms単位で数値を渡すことで生存期間設定します。
await kv.set(['hello', "deno"], 'good!', { expireIn: 5 });
返り値はPromise<KvCommitResult>
です。以下のようになっていて、結果とversionstamp
を渡してくれます。
type KvCommitResult = {
ok: true;
versionstamp: string;
}
delete
delete
は値の削除です。
await kv.delete(['hello', "deno"]);
第1引数にキーを渡して対象の値が存在すれば削除、なければ何もしません。返り値はPromise<void>
なので何も返しません。
トランザクション
Deno KVでトランザクションを張るにはatomic
関数を使います。
kv.atomic()
でトランザクションを開始してAtomicOperation
クラスを提供します。
AtomicOperation
クラスはcheck
やset
、commit
などのメソッドを持ちます(他にも色々な操作メソッドを持っています)。
check
は取得した値が現在も最新であることを確認します。取得した値は引数にAtomicCheck
のような形で渡します。
type AtomicCheck = {
key: KvKey;
versionstamp: string | null;
}
以下のようにして事前に取得したキーが['hello', 'deno']
の値が最新であることを確かめます。
const hd = await kv.get<string>(['hello', 'deno']);
kv.atomic().check(hd)
結果として自身(AtommicCheck
)を返して次に繋げます。
引数を調整して複数チェックできます。
const hd = await kv.get<string>(['hello', 'deno']);
// 意味ないですが、例として
kv.atomic().check(hd, hd, hd)
set
は先ほど見たset
と同じような引数を渡して値の作成または更新を行います。check
同様に返り値は自身(AtommicCheck
)を返します。
commit
はatomic
を呼び出してからcommit
が呼ばれるまでの一連の操作を実行します。
以下の例では事前に取得した値が最新であることを確かめてから作成・更新します。
const key = ['hello', 'deno']
const hd = await kv.get<string>(key);
await kv.atomic()
.check(hd)
.set(key, 'good')
.commit();
check
で矛盾した場合や作成・更新に失敗した場合はこれまでの操作を棄却します。
成功の有無はcommit
の返り値から判断可能です。型はPromise<KvCommitResult | KvCommitError>
のようになっていて、成功した時はKvCommitResult
を返し、失敗した時はKvCommitError
を返します。
type KvCommitResult = {
ok: true;
versionstamp: string;
}
type KvCommitError = {
ok: false;
}
さいごに
Deno KVの基本的な操作を紹介しました。Denoのエコシステムがどんどん発達していることに喜びを感じます。
かなり重要な機能だと思うので本番のリリースが楽しみです。