Azure Cosmos DB の無料枠が出来たので Cosmos DB に TypeScript から繋いで SQL 投げてみた

ということで、[前回](https://qiita.com/okazuki/items/71a08ef7b5a14a83eacb)に引き続き Cosmos DB 触っていきます。前回は Azure Functions 使ってみたけど、今回は素のコンソールアプリケーションから突いてみたいと思います。

Windows だと Cosmos DB にはローカル開発用のエミュレーターがあるけど Linux とか macOS 向けにはまだないのかな。Windows だと docker コンテナ上で実行したり

[ローカルでの開発とテストに Azure Cosmos Emulator を使用する](https://docs.microsoft.com/ja-jp/azure/cosmos-db/local-emulator)

```
上記ドキュメントから引用
## システム要件
Azure Cosmos Emulator のハードウェア要件とソフトウェア要件は、次のとおりです。
- ソフトウェア要件
  - Windows Server 2012 R2、Windows Server 2016、または Windows 10
  - 64 ビット オペレーティング システム
- 最小ハードウェア要件
  - 2 GB の RAM
  - 10 GB のハードディスク空き容量
```

無料枠が出来たので試すのにはあんまり困らないけど、ローカルで閉じて開発できるというのはやっぱり強いので Windows 以外への対応も個人的には早くほしいな～と思うところ。今回は本物使って試してみます。

## やってみよう

ということでやってみようと思います。Azure に作成した Cosmos DB は SQL を選択して作りました。これで作ると、JSON のコレクションに対して SQL 書けるのが強い。慣れ親しんだ `SELECT xxxx FROM xxxx WHERE xxxxx` という形でクエリを投げられるのは強い。

ということでまずは node のプログラムの下準備。Visual Studio Code で TypeScript で作っていこうと思います。
適当な空のフォルダーで以下のコマンドを叩きます。

```
> git init
> npm init -y
> code -r .
```

続けて TypeScript 関連のパッケージを追加して初期化

```
> npm install -D typescript @types/node
> tsc -init
```

tsconfig.json はちょっとだけいじりました。

```json
{
  "compilerOptions": {
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "target": "ES2020",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    "sourceMap": true,                     /* Generates corresponding '.map' file. */
    "outDir": "./dist",                        /* Redirect output structure to the directory. */
    "strict": true,                           /* Enable all strict type-checking options. */
    "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    "forceConsistentCasingInFileNames": true  /* Disallow inconsistently-cased references to the same file. */
  }
}
```

VS Code で `Ctrl + Shift + B` をしてビルドタスクで `tsc` するようにして、`F5` を押してデバッグするようにしたら準備完了！！試しに `index.ts` を作ってハローワールドしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/7bb9dc71-2592-61d3-5758-cd90a8449f2d.png)

ばっちりですね。では続けて Cosmos DB の SDK を入れていきましょう。公式にチュートリアルがあります。

[チュートリアル:JavaScript SDK を使用して、Azure Cosmos DB SQL API データを管理するための Node.js コンソール アプリを構築する](https://docs.microsoft.com/ja-jp/azure/cosmos-db/sql-api-nodejs-get-started)

SDK を npm でさくっといれちゃいましょう

```
> npm install @azure/cosmos --save
```

`node_modules/@azure/cosmos/dist` の下を見てみると index.d.ts とかもあるので安心して使えそうです。

接続のための情報をゲットします。Azure ポータルから作成した Cosmos DB を選択してキーから URI とプライマリ キーをコピーしましょう。`.env` ファイルを追加して以下のように記載しておきます。

```
COSMOS_DB_URI=ポータルからゲットした URI
COSMOS_DB_KEY=ポータルからゲットしたプリマり キー
```

dotenv を npm で入れます。

```
> npm install dotenv --save
```

では早速繋いで DB を作ってみましょう。DB を作るには CosmosClient クラスを作って databases の createIfNotExists で作れるようなのでさくっと呼んでみます。

```typescript
import { CosmosClient } from '@azure/cosmos';
// 環境変数を .env から読み込む
import dotenv from 'dotenv';
dotenv.config();

async function main() {
    const endpoint = process.env.COSMOS_DB_URI;
    const key = process.env.COSMOS_DB_KEY;
    if (!endpoint || !key) {
        return;
    }

    // Cosmos DB に繋ぐクライアントを作る
    const client = new CosmosClient({
        endpoint: endpoint,
        key: key,
    });

    // DB が無かったら作る
    const { database } = await client.databases.createIfNotExists({ id: 'dbforts' });
    console.log(`${database.id} was created.`);
}

main();
```
これを実行すると以下のような結果になりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/0a850bbe-70e4-a07b-c37a-e1d4b2629e1f.png)

いい感じですね。そのままコレクションも作ってみます。コレクションを作るには id (名前ですね) と partitionKey (ここで指定したプロパティの値でパーティション分割される）と、offerThroughput のようです。offerThroughput が高いほど性能がいいのですが無料枠に収めたいので最低値の 400 あたりを設定しておこうと思います。先ほどのコードの続きにコレクションを作るコードを追加しました。

```typescript
// コレクションが無かったら作る
const { container } = await database.containers.createIfNotExists({ 
    id: 'items', 
    partitionKey: { paths: [ '/partitionKey' ]}
}, 
{ offerThroughput: 400 });
    
console.log(`${container.id} was created.`);
```

実行してみると期待した通りに動きました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/9441eb47-29f3-4ae5-2e59-3982d5708af6.png)

Azure ポータルで、作成した Cosmos DB のデータ エクスプローラーを選択してみると、ちゃんと TypeScript で指定した DB とコレクションが出来ています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/6f8fc15a-6078-af43-d2b2-336de62965f2.png)

じゃぁ適当にデータを突っ込んでいきましょう。 pk1 と pk2 という 2 種類の partitionKey を持つ感じで、それぞれに 100 件ずつ適当にデータを突っ込みます。

```typescript
import { CosmosClient } from '@azure/cosmos';
// 環境変数を .env から読み込む
import dotenv from 'dotenv';
import { Hash } from 'crypto';
dotenv.config();

// Cosmos DB に突っ込む型
type Item = {
    id: string | undefined,
    value: string,
    partitionKey: string,
};

async function main() {
    const endpoint = process.env.COSMOS_DB_URI;
    const key = process.env.COSMOS_DB_KEY;
    if (!endpoint || !key) {
        return;
    }

    // Cosmos DB に繋ぐクライアントを作る
    const client = new CosmosClient({
        endpoint: endpoint,
        key: key,
    });

    // DB が無かったら作る
    const { database } = await client.databases.createIfNotExists({ id: 'dbforts' });
    console.log(`${database.id} was created.`);

    // コレクションが無かったら作る
    const { container } = await database.containers.createIfNotExists({ 
        id: 'items', 
        partitionKey: { paths: [ '/partitionKey' ]}
    }, 
    { offerThroughput: 400 });
    
    console.log(`${container.id} was created.`);

    // 適当なデータを突っ込む
    for (let i = 0; i < 100; i++) {
        const { item: item1 } = await container.items.upsert({
            partitionKey: 'pk1',
            value: `${new Date().toISOString()} ${Math.random() * 1000}`,
        });
        const { item: item2 } = await container.items.upsert({
            partitionKey: 'pk2',
            value: `${new Date().toISOString()} ${Math.random() * 1000}`,
        });
        console.log(`${item1.id} and ${item2.id} were created.`);
    }
}

main();
```

実行すると、こんな感じでログが出ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/ddb1f05a-26f9-9242-cbfa-a082c2889c74.png)

データエクスプローラーで覗いてみるとちゃんとデータが出来ています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/264dbb62-0494-6759-bef5-4c60c8ebe40f.png)

ついに SQL を実行します。Cosmos DB で使える SQL は以下のページにまとまってます。

[SQL クエリの使用を開始する](https://docs.microsoft.com/ja-jp/azure/cosmos-db/sql-query-getting-started)

部分一致で value に 000 が入っているものだけ抜いてみようと思います。ちょっとしくじったなと思ったのは value が SQL のキーワードで SQL 内で value プロパティの値を扱おうとするとキーワードなのでエラーになってしまうので `c["value"]` のような表記にしないといけなかったところです。キーワードをプロパティ名にしてない場合は `c["value"]` は `c.value` のように記載できます。

ではさくっと SQL を発行してみましょう。パラメーターもサポートしているので SQL インジェクションも安心ですね。

```typescript
import { CosmosClient } from '@azure/cosmos';
// 環境変数を .env から読み込む
import dotenv from 'dotenv';
import { Hash } from 'crypto';
dotenv.config();

// Cosmos DB に突っ込む型
type Item = {
    id: string | undefined,
    value: string,
    partitionKey: string,
};

async function main() {
    const endpoint = process.env.COSMOS_DB_URI;
    const key = process.env.COSMOS_DB_KEY;
    if (!endpoint || !key) {
        return;
    }

    // Cosmos DB に繋ぐクライアントを作る
    const client = new CosmosClient({
        endpoint: endpoint,
        key: key,
    });

    // DB が無かったら作る
    const { database } = await client.databases.createIfNotExists({ id: 'dbforts' });
    console.log(`${database.id} was created.`);

    // コレクションが無かったら作る
    const { container } = await database.containers.createIfNotExists({ 
        id: 'items', 
        partitionKey: { paths: [ '/partitionKey' ]}
    }, 
    { offerThroughput: 400 });
    
    console.log(`${container.id} was created.`);

    // 適当なデータを突っ込む
    // for (let i = 0; i < 100; i++) {
    //     const { item: item1 } = await container.items.upsert({
    //         partitionKey: 'pk1',
    //         value: `${new Date().toISOString()} ${Math.random() * 1000}`,
    //     });
    //     const { item: item2 } = await container.items.upsert({
    //         partitionKey: 'pk2',
    //         value: `${new Date().toISOString()} ${Math.random() * 1000}`,
    //     });
    //     console.log(`${item1.id} and ${item2.id} were created.`);
    // }

    const { resources } = await container.items.query<Item>({
        query: 'SELECT * FROM items c WHERE CONTAINS(c["value"], @xxx)',
        parameters: [
            { name: '@xxx', value: '000' }
        ]
    }).fetchAll();
    for (let x of resources) {
        console.log(`id:${x.id}, value:${x.value}, partitionKey:${x.partitionKey}`);
    }
}

main();
```

実行すると私の場合ははかったかのように pk1, pk2 それぞれに 1 件ずつありました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/10083476-5d2b-8db6-fa0b-469108a220e9.png)

`fetchAll` の戻り値の `requestCharge` で消費 RU もわかります。

```typescript
const { resources, requestCharge } = await container.items.query<Item>({
    query: 'SELECT * FROM items c WHERE CONTAINS(c["value"], @xxx)',
    parameters: [
        { name: '@xxx', value: '000' }
    ]
}).fetchAll();
// RU を表示
console.log(`requestCharge: ${requestCharge}`);
for (let x of resources) {
    console.log(`id:${x.id}, value:${x.value}, partitionKey:${x.partitionKey}`);
}
```

その他に `query` の第二引数に `FeedOptions` も指定可能で SQL 件数が多いときに続きから読み込むとかできます。

[FeedOptions interface](https://docs.microsoft.com/ja-jp/javascript/api/@azure/cosmos/feedoptions?view=azure-node-latest)

びっくりしたのが、partitionKey でフィルタリングしていないとクロスパーティションクエリ―になるのですが、C# 用 SDK だと明示的に `FeedOptions` で `EnableCrossPartitionQuery` に `true` を指定しないとエラーになった気がするのですが、JavaScript SDK だと勝手にクロスパーティションクエリーになるのかな？

実際に使うときには `WHERE` には可能な限りパーティションキーつけて、データの設計をするときになるべくクエリ―の範囲はパーティションキーの範囲内に入るようにしつつ、いい感じにデータは分散されるようにしておくのがいいとされています（超ムズイ

詳細は以下のドキュメントを参照してください。
[Azure Cosmos DB でのパーティション分割](https://docs.microsoft.com/ja-jp/azure/cosmos-db/partitioning-overview)

## まとめ

ということで、前回は Azure Functions のバインドの機能を使ってやってましたが、ちゃんと JavaScript 用の SDK もあって繋いでデータ突っ込んだりクエリ投げたりできます。
なので express 製の Web アプリからもサクッと使えるので Azure App Service の Web App の無料プランで express のアプリをホストして裏で Cosmos DB の無料プランという構成も出来そうですね。

今回書いたソースコードは以下の GitHub のリポジトリーにアップしています。

https://github.com/runceel/hello-cosmos

それでは、良い Cosmos DB ライフを。