17
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-13

ということで、前回に引き続き Cosmos DB 触っていきます。前回は Azure Functions 使ってみたけど、今回は素のコンソールアプリケーションから突いてみたいと思います。

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

ローカルでの開発とテストに Azure Cosmos 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 はちょっとだけいじりました。

{
  "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

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

チュートリアル:JavaScript SDK を使用して、Azure Cosmos DB SQL API データを管理するための Node.js コンソール アプリを構築する

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 で作れるようなのでさくっと呼んでみます。

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

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

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

console.log(`${container.id} was created.`);

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

image.png

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

image.png

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

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

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

image.png

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

SQL クエリの使用を開始する

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

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

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

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

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

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

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

詳細は以下のドキュメントを参照してください。
Azure Cosmos DB でのパーティション分割

まとめ

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

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

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

17
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
7