Craft Key-Value Store にデータを保存する
Craft Key-Value Store (Craft KVS) はCraft Functions内で利用できるKey Value Storeです。
Craft Functionsの MODULES.kvs から利用します。
KVSの構造
Craft KVSのレコードはkey と value からなります。
key- KVSのユニークなキーです
- KARTEプロジェクト単位でユニークなキーとなります。
MODULES.kvs上はstring型の値として扱います- マルチバイト文字は利用できません
- KVSのユニークなキーです
value- KVSのキーに紐づく値です
MODULES.kvsでは一般的なオブジェクトとして扱います。
また、Craft KVSのレコードは管理用の情報として created_at , expired_at フィールドを持ちます。
created_at- レコードの作成/更新時刻です。
- 書き込み系のメソッドを実行したタイミングで自動的に付与されます。
- レコードを更新(上書き)した場合、
created_atフィールドも更新されます。
- レコードを更新(上書き)した場合、
expired_at- レコードの有効期限です。
- 書き込み系のメソッドで指定します。
expired_atの時刻を超えたレコードは、通常72時間以内にKVSが自動で削除します。
サンプルコード
export default async function (data, {MODULES}) { const {kvs} = MODULES; const key = "key1";
// KVSにレコードを書き込む await kvs.write({ key, value: { [key]: 123 }});
// 単一レコードを取得する(deprecated) await kvs.get({ key });
// 複数レコードを一度に取得する(推奨) await kvs.get({ keys: ['key1', 'key2', 'key3'] });
// unixtimeMsよりも前に更新されていればレコードを更新する const unixtimeMs = new Date().getTime(); await kvs.checkAndWrite({ key, value:{ [key]: 1234 }, operator: "<", unixtimeMs})
// レコードを削除する await kvs.delete({ key });}メソッド
kvs モジュールで利用できるメソッドについて説明します。
write
/** * @param {Object} param * @param {string} param.key キー * @param {Object} param.value 値 * @param {number} [param.minutesToExpire] 有効期限(分) * @returns {Promise<Object>} */kvs.write({ key, value, minutesToExpire })storeにデータを書き込みます。
1 valueあたり 1KB の制限があります。
minutesToExpireで何分後にexpireするか指定することが出来ます。minutesToExpireを指定しない場合はTTLはデフォルトで1 dayです。
minutesToExpireの上限は、通常のプランでは現在時刻から1 month(44,640min)までです。これを超える値を指定したい場合は、プレイドの営業・カスタマーサクセスにお問い合わせください。
get
/** * @param {Object} param * @param {string} [param.key] キー(deprecated, use keys instead) * @param {string[]} [param.keys] キーの配列(複数取得、最大100件) * @returns {Promise<Object>} 単一の場合は{key: value}、複数の場合は各keyの結果(存在しないキーはnull) */kvs.get({ key }) // deprecatedkvs.get({ keys }) // 推奨storeからデータを読み取ります。
単一キー取得(deprecated):
const result = await kvs.get({ key: 'key1' });複数キー取得(推奨):
const result = await kvs.get({ keys: ['key1', 'key2', 'key3'] });// 結果: { key1: {...}, key2: {...}, key3: null } // 存在しないキーはnull
// 重複するキーは自動的に除去されるconst result2 = await kvs.get({ keys: ['key1', 'key1', 'key2'] });// 結果: { key1: {...}, key2: {...} } // key1は1つのレコードのみ返されるなお、TTLによるレコードの自動削除は expired_at で指定した時刻から72時間後までの間に行われるため 、expired_atに過去の値を持つデータを取得することがあります。
delete
/** * @param {Object} param * @param {string} param.key キー * @returns {Promise<Object>} 空オブジェクト */kvs.delete({ key })storeからデータを削除します。
checkAndWrite
/** * 条件付きでデータを書き込みます。 * * @param {Object} param * @param {string} param.key キー * @param {Object} param.value 値 * @param {string} param.operator 比較演算子(`<`, `>`, `=`, `<=`, `>=` のいずれか) * @param {number} param.unixtimeMs 比較対象のunixtime(ms)(ミリ秒) * @param {number} [param.minutesToExpire] 値の有効期限(分) * @returns {Promise<Object>} 空オブジェクト */kvs.checkAndWrite({ key, value, operator, unixtimeMs, minutesToExpire })created_at fieldに対する条件を照らしてから、その結果に応じて条件付き書き込みができます。
operator は、 =, <, <=, > , >= から指定できます。
unixtimeMs は const unixtimeMs = new Date().getTime() のように millisecond で指定ください。
条件判定では、created_at, operator, unixtimeMs の関係が以下となる場合に書き込みを行います。
created_at (operator) unixtimeMs例えばoperatiorに ‘<’ を指定した場合、 created_at が unixtimeMs より小さい場合に値を書き込みます。
created_at < unixtimeMsレコードが現在時刻の5分前よりも前に作成・更新された場合にのみ上書きするには、次の条件式になるように引数を設定します。
created_at < (unixtimeMs - 300000)この時のメソッド呼び出しは次のようになります。
const currentTime = new Date().getTime()await kvs.checkAndWrite({ key: 'key' value: { id: 'hoge', name: 'fuga' } operator: '<' unixtimeMs: currentTime - 300000});なお、TTLの挙動はwrite methodと同じです。
checkAndWriteのエラーハンドリング
throwされるerrorオブジェクトのstatus というフィールドに次のステータスコードが代入されます。要件に応じてretryの処理を入れてください。
- status: 404
- writeメソッド、checkAndWriteメソッドにおいて、条件に合致するデータが存在せず、値が更新されなかった場合に返却されます。
- データ自体が存在しない場合、このerrorは発生せずwriteが成功します。
- status: 409
- 同時に同じキーが更新されたので更新されなかった
- status: 400
- その他のバリデーションエラーなど
- status: 500
- Internal Error
list
/** * 条件付きでデータを書き込みます。 * * @param {Object} param * @param {string} param.startCursor 取得するcursorの開始位置 * @param {number} param.pageSize 取得する件数(最大30件) * @param {string} param.srartKey 取得するkeyの開始位置 * @param {string} param.stopKey 取得するkeyの終了位置 * @returns {Promise<{item: KeyValueStoreSet, endCursor: string, isMoreResults: boolean}>} */kvs.list({ startCursor, pageSize, startKey, stopKey })storeから、最大30件まで複数のデータを読み取ります。
30件を超えるデータを読み取る場合は、戻り値のisMoreResultsがtrueとなり、endCursorの値を次の読み取り時のstartCursorに指定することで次の30件を読み取ることができます。
読み取るデータの順序はkeyの文字列昇順になります。
startKeyとstopKeyに読み取るkeyを文字列で範囲指定できます。
例えばKVSに登録しているデータのkeyがkey-sample-001からkey-sample-099の場合、次のように指定すると10件のデータに絞り込んで読み取ることができます。
startKey:key-sample-001stopKey:key-sample-010
なお文字列の昇順検索となるため、Hash値でkeyを指定している場合は範囲指定が難しくなります。
keyのサフィックスには数字を指定することが望ましいでしょう。
またサフィックスの数字の最大値が不明である場合は、例えばstopKey : key-sample-aというようにサフィックスを文字列指定することで全件読み取ることができます。
サンプルデータ
- 自動で
[key名].valueというパスが間に入ります。
await kvs.write({ key:'current_coupon', value: { index: 0, user_id: null }});{ "current_coupon": { "created_at": "2022-12-28T02:50:57.167Z", "expired_at": "2023-01-27T02:50:57.167Z", "value": { "index": 0, "user_id": null } }}// 単一キー取得(deprecated)const v = await kvs.get({ key: 'current_coupon' });const current_index = v.current_coupon.value.index;
// 複数キー取得(推奨)const results = await kvs.get({ keys: ['current_coupon', 'user_settings', 'config'] });const current_index = results.current_coupon?.value?.index;const user_settings = results.user_settings?.value;// 存在しないキーはnullが返されるconst config = results.config; // null if not exists制限
1 valueあたり 1KB の制限があります。
API リファレンス
注意点
ホットスポットの発生によるパフォーマンス低下
keyの辞書順が近いレコード群に対して頻繁に読み取りや書き込みを行うと、ホットスポットの発生により一時的にパフォーマンス低下することがあります。
この問題は、keyに対して辞書順をばらけさせるためのprefixを付与することで軽減できる可能性があります。
// keyにホットスポット回避用のprefixを付与する実装の例
import crypto from 'crypto';
function generateHashPrefix(id) { const hashBase64 = crypto.createHash('sha256').update(id).digest('base64'); // 辞書順を分散させるためハッシュ値の5〜12文字目を使用 const prefix = hashBase64.substring(4, 12); return prefix;}
function kvsKey(id) { const hash = generateHashPrefix(id); return `${hash}-${id}`;}FAQ
Q. KVSはどの単位で共有されますか?
- プロジェクト単位で1つのKVSが共有されます。
- 複数のFunctionで同じデータが参照できてしまいます。
- Function単位でnamespaceを厳密に管理してください。
Q. すでに存在するkeyに対してwriteをした場合はどうなりますか?
- 既存のレコードが上書きされます
- 内部的には新しくレコードが作り直される形になり、created_atも最新の時刻で更新されます
Q. レコードの有効期限(minutesToExpire)を指定せずにレコードを永続化できますか?
minutesToExpireを設定せずにレコードを書き込んだ場合は、永続化されるのではなく上述した「デフォルトの有効期限」が設定されますminutesToExpireに設定できる値の上限は契約プランによって異なりますが、仮に設定値の上限が無いプロジェクトであっても、有効期限を長く設定したい場合はminutesToExpireを明示的に指定してください- その場合、
minutesToExpireを1000年後 (約525,600,000分後) など遠い未来の値に指定することで、実質的にレコードの永続化が可能です
- その場合、