> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twenty.com/llms.txt
> Use this file to discover all available pages before exploring further.

# キーバリューストア

> シンプルなキーと値のオブジェクトを使って、中間結果を永続化し、データをキャッシュし、ロジック関数の実行間で状態を共有します。

ロジック関数は短命な Node.js プロセス内でサンドボックス実行されます。1 回の実行が終了すると、メモリ上に保持されていたものは何も残りません。 実行間で**何かを記憶しておく必要がある**場合（高コストな API レスポンスのキャッシュ、増分同期のためのカーソルの保存、処理のデバウンス、ある関数から別の関数への状態の受け渡しなど）、ワークスペースのデータベースに永続化します。

これには専用のストレージプリミティブは必要ありません。`key` フィールドと `value` フィールドを持つ小さな**技術的なオブジェクト**があれば、ワークスペースをスコープとした永続的なキーバリューストアになり、既にレコード用に使用しているのと同じ [型付き API クライアント](/l/ja/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk)からクエリできます。

```text theme={null}
  ┌─────────────────┐   set(key, value)    ┌──────────────────────────┐
  │ Logic function  │ ───────────────────▶ │ "KV Store" object        │
  │ (your handler)  │ ◀─────────────────── │  key (unique)  │  value  │
  └─────────────────┘   get(key)           └──────────────────────────┘
```

## ストアオブジェクトを定義する

2 つのフィールドを持つカスタムオブジェクトを宣言します — `key`（一意な `TEXT`）と `value`（任意の JSON シリアライズ可能なペイロードを保存できるようにするための `RAW_JSON`）。 完全な `defineObject` のリファレンスについては [Objects](/l/ja/developers/extend/apps/data/objects) を参照してください。

```ts src/objects/kv-store.object.ts theme={null}
import { defineObject, FieldType } from 'twenty-sdk/define';

export const KV_STORE_UNIVERSAL_IDENTIFIER =
  '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33';
export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER =
  '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21';
export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER =
  '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58';

export default defineObject({
  universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER,
  nameSingular: 'kvStore',
  namePlural: 'kvStores',
  labelSingular: 'KV Store',
  labelPlural: 'KV Store',
  description: 'Key-value storage for logic functions',
  icon: 'IconDatabase',
  fields: [
    {
      universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER,
      name: 'key',
      type: FieldType.TEXT,
      label: 'Key',
      description: 'Unique lookup key',
      icon: 'IconKey',
    },
    {
      universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER,
      name: 'value',
      type: FieldType.RAW_JSON,
      label: 'Value',
      description: 'Stored JSON payload',
      icon: 'IconJson',
    },
  ],
});
```

### キーの一意性を保証する

`key` に対して**一意インデックス**を追加し、同じキーに 2 行が割り当てられないようにします。 これは一意性のために推奨されるプリミティブです。詳細は [Data → Unique indexes](/l/ja/developers/extend/apps/data/overview#unique-indexes) を参照してください。

```ts src/indexes/kv-store-key.index.ts theme={null}
import { defineIndex } from 'twenty-sdk/define';
import {
  KV_STORE_UNIVERSAL_IDENTIFIER,
  KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/kv-store.object';

export default defineIndex({
  universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14',
  objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER,
  isUnique: true,
  fields: [
    {
      universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15',
      fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER,
    },
  ],
});
```

## ロジック関数から読み書きする

オブジェクトをいくつかの小さなヘルパーでラップし、残りのコードを `get`、`set`、`del` を持つキーバリュー API のように記述できるようにします。 これらは [`CoreApiClient`](/l/ja/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) を使用します。これはワークスペースのスキーマから生成され、`kvStore` オブジェクトに対して完全に型付けされています。

```ts src/logic-functions/handlers/kv-store.ts theme={null}
import { CoreApiClient } from 'twenty-client-sdk/core';
import { isDefined } from 'twenty-sdk/utils';

const client = new CoreApiClient();

// Look up a single row by its key.
const findByKey = async (key: string) => {
  const { kvStores } = await client.query({
    kvStores: {
      __args: { filter: { key: { eq: key } }, first: 1 },
      edges: { node: { id: true, value: true } },
    },
  });

  return kvStores.edges[0]?.node;
};

// Read a value. Returns undefined when the key is missing.
export const get = async <TValue>(key: string): Promise<TValue | undefined> => {
  const row = await findByKey(key);

  return isDefined(row) ? (row.value as TValue) : undefined;
};

// Write a value. Creates the row on first write, updates it afterwards (upsert).
export const set = async (key: string, value: unknown): Promise<void> => {
  const existing = await findByKey(key);

  if (isDefined(existing)) {
    await client.mutation({
      updateKvStore: {
        __args: { id: existing.id, data: { value } },
        id: true,
      },
    });
    return;
  }

  await client.mutation({
    createKvStore: {
      __args: { data: { key, value } },
      id: true,
    },
  });
};

// Delete a value. No-op when the key is missing.
export const del = async (key: string): Promise<void> => {
  const existing = await findByKey(key);

  if (isDefined(existing)) {
    await client.mutation({
      deleteKvStore: { __args: { id: existing.id }, id: true },
    });
  }
};
```

<Note>
  一意インデックスは重複を防ぎますが、**同じ新しいキー**を 2 つの実行がまったく同時に書き込もうとした場合、ルックアップと作成の間で競合状態が発生する可能性があります。 作成が一意制約で失敗した場合は「他の誰かが勝った」と見なし、それをキャッチして再読み込みするか、更新としてリトライします。
</Note>

## 使ってみる：高コストな呼び出しをキャッシュする

典型的な用途としては、遅い、またはレート制限されたサードパーティのレスポンスをキャッシュしておき、毎回コストを支払うのではなく、繰り返しの実行で再利用することが挙げられます。

```ts src/logic-functions/getExchangeRate.logic-function.ts theme={null}
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';

const ONE_HOUR_MS = 60 * 60 * 1000;

type CachedRate = { rate: number; fetchedAt: number };

const handler = async (params: { from: string; to: string }) => {
  const cacheKey = `exchange-rate:${params.from}:${params.to}`;
  const cached = await get<CachedRate>(cacheKey);

  if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
    return { rate: cached.rate, cached: true };
  }

  const response = await fetch(
    `https://api.example.com/rate?from=${params.from}&to=${params.to}`,
  );
  const { rate } = (await response.json()) as { rate: number };

  await set(cacheKey, { rate, fetchedAt: Date.now() });

  return { rate, cached: false };
};

export default defineLogicFunction({
  universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
  name: 'get-exchange-rate',
  timeoutSeconds: 10,
  handler,
});
```

## パターン & ヒント

* **ネームスペース化。** さまざまな用途を分離し、まとめてルックアップしやすくするために、キーにプレフィックスを付けます — `sync-cursor:linear`、`cache:exchange-rate:USD:EUR`、`lock:nightly-report`。 `key: { like: 'cache:%' }` でフィルタリングして、ネームスペース全体を一覧表示またはクリアします。
* **期限 (TTL)。** ストアには組み込みの有効期限はありません。 `value` 内にタイムスタンプを保存して（キャッシュの例のように）読み取り時にチェックするか、`DATE_TIME` フィールドを追加し、[cron-triggered function](/l/ja/developers/extend/apps/logic/logic-functions) から定期的に古い行をクリアします。
* **何を保存するか。** `RAW_JSON` には、数値、文字列、配列、オブジェクトなど、任意の JSON シリアライズ可能な値を格納できます。 エントリは小さく保ってください。これは調整やキャッシュのためのものであり、大きな BLOB やファイル用ではありません。 ファイルには `FILES` フィールドと [`uploadFile`](/l/ja/developers/extend/apps/logic/logic-functions#uploading-files) を使用してください。
* **可視性と権限。** 行は他のレコードと同様にワークスペースのデータベース内に存在するため、API を通じてクエリでき、アプリの [role](/l/ja/developers/extend/apps/config/roles) に従った権限が適用されます。 ストアをメイン UI から隠しておきたい場合は、[navigation menu](/l/ja/developers/extend/apps/layout/navigation-menu-items) に追加しないでください。
* **レコードへのスコープ。** グローバルなキーではなく、レコード単位の状態が必要ですか？ ストアオブジェクトから対象オブジェクトへの [relation](/l/ja/developers/extend/apps/data/relations) を追加し、id をキーにエンコードするのではなく関連付けを使います。

<Note>
  これは約束事であり、別個の機能ではありません。「KV Store」は、標準の API で定義およびクエリする、通常のカスタムオブジェクトにすぎません。 つまり、アプリの他のデータと同様に、同じ同期、権限、ツール群の恩恵を受けられます。
</Note>
