この記事について
本記事は、2020年3月6日 (米国時間) にて、Azure Cosmos DB に新しく Free Tier (無償利用枠) が登場したことに伴い、改めて Azure Cosmos DB を色々と触っていく試みの 5 回目です。
今回も、前回記事 同様、 Microsoft Azure Cosmos JavaScript SDK について見ていきたいと思います。
- 2020年から始めるAzure Cosmos DB - JavaScript SDK (SQL API)を見てみる (Part.1)
- 2020年から始めるAzure Cosmos DB - JavaScript SDK (SQL API)を見てみる (Part.2)
対象読者
- Azure Cosmos DB について学習したい方
- Node.js で Azure Cosmos DB への CRUD 操作を行いたい方
- Microsoft Azure Cosmos JavaScript SDK の動作について理解したい方
Microsoft Azure Cosmos JavaScript SDK
実際に、Microsoft Docs の内容を元に、JavaScript SDK (SQL API) の中身を見ていきます。
今回はコンテナーの読み取り、または削除
を行う Container について確認します。
Container
TypeDoc の記載は、以下の通りです。
Operations for reading, replacing, or deleting a specific, existing container by id.
特定の既存のコンテナーをIDで読み取り、置換、または削除するための操作。
const client: CosmosClient = new CosmosClient({ endpoint, key });
const database: Database = client.database(databaseId);
const container: Container = database.container(containerId);
実際に使用する際は、CosmosClient および Databse クラスを生成した後、Database クラス内にあるcontainer
メソッドを使用して、Container クラスを生成する手順が必要になります。
Database クラスのcontainer
メソッドは、どうなっているのか、実際に中身を確認してみます。
public container(id: string): Container {
return new Container(this, id, this.clientContext);
}
はい、Container クラスのコンストラクタが動いています。
前回 同様、Container 型の定数(const)に値を代入しているのだから当たり前ですよね。
実際にコンストラクタの中身を見ていきます。
export class Container {
private $items: Items;
constructor(
public readonly database: Database,
public readonly id: string,
private readonly clientContext: ClientContext
) {}
}
はい、ここでも Database クラスのコンストラクタの時と同様、パラメータプロパティ宣言
がまた利用されています。
コンストラクタの引数の中で、プロパティの作成および初期化が行えるようになるというものでしたね。
つまり、コンストラクタの内容は
export class Container {
private $items: Items;
public readonly database: Database;
public readonly id: string;
private readonly clientContext: ClientContext;
constructor(
database: Database,
id: string,
clientContext: ClientContext
) {
this.database = database;
this.id = id;
this.clientContext = clientContext;
}
}
と同じ内容ということになります。
実際にコンパイルされた後の JavaScript ファイルも見てみましょう。
class Container {
constructor(database, id, clientContext) {
this.database = database;
this.id = id;
this.clientContext = clientContext;
}
}
パラメータプロパティ宣言にあるとおり、プロパティに対して引数の値を代入していますね。同じ内容であることを確認できました。
Containers
前回 の記事にて、Database
クラスのコンストラクタを実行した際に、Containers クラスの初期化を行っていたのを覚えているでしょうか。
export class Database {
public readonly containers: Containers;
public readonly users: Users;
constructor(
public readonly client: CosmosClient,
public readonly id: string,
private clientContext: ClientContext
) {
this.containers = new Containers(this, this.clientContext);
this.users = new Users(this, this.clientContext);
}
}
Database クラスのプロパティとして保持されている、Containers
クラスについてもみていこうと思います。
コンストラクタの内容は、前回記事で紹介した通り、以下のようになっています。
export class Containers {
constructor(public readonly database: Database, private readonly clientContext: ClientContext) {}
}
見てお分かりの通り、ここでも パラメータプロパティ宣言 が使用されていますね。
Containers クラスの TypeDoc を見てみましょう。
Operations for creating new containers, and reading/querying all containers
新しいコンテナーを作成し、すべてのコンテナを読み取り/クエリするための操作
Container s とあるように、Container クラスと違うのは、全てのコンテナーの情報の読み取り/書き込みを行う
ということですね。
読み取りの部分で言うと、実際にreadAll
メソッドが用意されており、Azure Cosmos DB アカウント以下にあるコンテナーの一覧情報を返すメソッドがあります。
public readAll(options?: FeedOptions): QueryIterator<ContainerDefinition & Resource> {
return this.query(undefined, options);
}
public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator<any>;
public query<T>(query: SqlQuerySpec, options?: FeedOptions): QueryIterator<T> {
const path = getPathFromLink(this.database.url, ResourceType.container);
const id = getIdFromLink(this.database.url);
return new QueryIterator(this.clientContext, query, options, (innerOptions) => {
return this.clientContext.queryFeed<ContainerDefinition>({
path,
resourceType: ResourceType.container,
resourceId: id,
resultFn: (result) => result.DocumentCollections,
query,
options: innerOptions
});
});
}
実際に、このquery
メソッドの TypeDoc を見てみましょう。
/**
* Queries all containers.
* すべてのコンテナーにクエリを実行します。
* @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query.
* 操作のためのクエリの構成。クエリの構成方法の詳細については、{@ link SqlQuerySpec}を参照してください。
* @param options Use to set options like response page size, continuation tokens, etc.
* 応答ページのサイズ、継続トークンなどのオプションを設定するために使用します。
* @returns {@link QueryIterator} Allows you to return specific contaienrs in an array or iterate over them one at a time.
* 特定のコンテナーを配列で返すか、一度に1つずつ反復することができます。
*/
というようになっています。
また、存在していないコンテナーの作成についても、この Containers クラス内のcreateIfNotExists
メソッドを利用して行うことができます。
public async createIfNotExists(
body: ContainerRequest,
options?: RequestOptions
): Promise<ContainerResponse> {
if (!body || body.id === null || body.id === undefined) {
throw new Error("body parameter must be an object with an id property");
}
/*
1. Attempt to read the Database (based on an assumption that most databases will already exist, so its faster)
データベースを読み取ろうとします(ほとんどのデータベースがすでに存在するという前提に基づいているため、より高速です)
2. If it fails with NotFound error, attempt to create the db. Else, return the read results.
NotFound エラーで失敗した場合は、データベースの作成を試みます。それ以外の場合は、読み取り結果を返します。
*/
try {
const readResponse = await this.database.container(body.id).read(options);
return readResponse;
} catch (err) {
if (err.code === StatusCodes.NotFound) {
const createResponse = await this.create(body, options);
// Must merge the headers to capture RU costskaty
mergeHeaders(createResponse.headers, err.headers);
return createResponse;
} else {
throw err;
}
}
}
public async create(
body: ContainerRequest,
options: RequestOptions = {}
): Promise<ContainerResponse> {
const err = {};
if (!isResourceValid(body, err)) {
throw err;
}
const path = getPathFromLink(this.database.url, ResourceType.container);
const id = getIdFromLink(this.database.url);
if (body.throughput) {
options.initialHeaders = Object.assign({}, options.initialHeaders, {
[Constants.HttpHeaders.OfferThroughput]: body.throughput
});
delete body.throughput;
}
// If they don't specify a partition key, use the default path
if (!body.partitionKey || !body.partitionKey.paths) {
body.partitionKey = {
paths: [DEFAULT_PARTITION_KEY_PATH]
};
}
const response = await this.clientContext.create<ContainerRequest>({
body,
path,
resourceType: ResourceType.container,
resourceId: id,
options
});
const ref = new Container(this.database, response.result.id, this.clientContext);
return new ContainerResponse(response.result, response.headers, response.code, ref);
}
あれ、、メソッド内に何か記載がされていますね。。メソッド内の説明によると、try-catch 構文の中で、
- すべてのデータベースの読み取りを実行し、指定したデータベースが存在するか確認
- 指定したデータベースが存在しなければ新規作成を行い、存在していれば読み取り結果を返す
ということを行っているそうです。コンテナーの話はどこいった・・・
(説明がどうもややこしい感じがしますが) この説明で注意したい部分は、今見ているのがContainers
クラスだ、ということです。
そして、この Containers クラスのメソッドを実行しようとするならば、通常は、その前段で Database クラスを生成しているはずなのです。
createIfNotExists で行うのはコンテナーの作成のはずです。念のため、TypeDoc も見てみましょう。
/**
* Checks if a Container exists, and, if it doesn't, creates it.
* コンテナーが存在するかどうかを確認し、存在しない場合は作成します。
* This will make a read operation based on the id in the `body`, then if it is not found, a create operation.
* これにより、`body`の ID に基づいて読み取り操作が行われ、見つからない場合は作成操作が行われます。
* You should confirm that the output matches the body you passed in for non-default properties (i.e. indexing policy/etc.)
* デフォルト以外のプロパティ(すなわちインデックス作成ポリシーなど)については、出力が渡された`body`と一致していることを確認する必要があります。
*
* A container is a named logical container for items.
* コンテナーとは、アイテムの名前付き論理コンテナーのことです。
* A database may contain zero or more named containers and each container consists of zero or more JSON items.
* データベースには 0 個以上の名前付きコンテナーを含めることができ、各コンテナーは 0 個以上の JSON アイテムで構成されます。
* Being schema-free, the items in a container do not need to share the same structure or fields.
* スキーマフリーであるため、コンテナー内のアイテムは同じ構造またはフィールドを共有する必要がありません。
* Since containers are application resources, they can be authorized using either the master key or resource keys.
* コンテナーはアプリケーションリソースであるため、マスターキーまたはリソースキーを使用して承認できます
*
* @param body Represents the body of the container.
* コンテナーの本体を表します。
* @param options Use to set options like response page size, continuation tokens, etc.
* 応答ページのサイズ、継続トークンなどのオプションを設定するために使用します。
*/
Checks if a Container exists, and, if it doesn't, creates it.
コンテナーが存在するかどうかを確認し、存在しない場合は作成します。
こちらにはきちんと書かれていましたね。と言うわけで、実際に動かして確認してたほうが早いので、動かしてみます。
任意のディレクトリでnpm init -y
を行い、index.js を作成します。
cd ~ #ここは好きなディレクトリを指定してください
npm init -y
npm install -y @azure/cosmos
'use strict'
const CosmosClient = require("@azure/cosmos").CosmosClient;
const config = {
endpoint: "<読み取り/書き込みキーのURIを入力>",
key: "<プライマリ キー あるいは セカンダリ キーを入力>",
databaseId: "<既に存在しているデータベース名を入力>",
};
async function main() {
const { endpoint, key, databaseId } = config;
const client = new CosmosClient({ endpoint, key });
const database = await client.database(databaseId);
// もし Tasks コンテナーが既に実在する場合は、任意の id に書き換えてください。
const res = await database.containers.createIfNotExists(
{ id: "Tasks", partitionKey: { paths: ["/category"]} },
{ offerThroughput: 400 }
).catch(e => {
console.error(e);
});
}
main();
node index.js
config 内の値が正常に入力されていれば、実在する Azure Cosmos DB のデータベース以下に、新しくTasks
コンテナーが作成されるはずです。
では逆に、config内で指定しているデータベースを実在しないものにした場合は、どうなるかと言うと、
Error: Message: {"Errors":["Resource Not Found"]}
ActivityId: 93b991e0-f761-4ceb-8028-e076813b0f49, Request URI: /apps/4d851b1c-a3a8-4e9f-8f64-eb20a41298cd/services/72192d2e-f151-46b6-b166-d40d65173948/partitions/13f2ad2f-6b39-45c4-a052-c3fca79a7a69/replicas/132358627723435031s, RequestStats:
RequestStartTime: 2020-06-07T11:49:26.7346281Z, RequestEndTime: 2020-06-07T11:49:26.7346281Z, Number of regions attempted:1
ResponseTime: 2020-06-07T11:49:26.7346281Z, StoreResult: StorePhysicalAddress: rntbd://10.0.0.29:11300/apps/4d851b1c-a3a8-4e9f-8f64-eb20a41298cd/services/72192d2e-f151-46b6-b166-d40d65173948/partitions/13f2ad2f-6b39-45c4-a052-c3fca79a7a69/replicas/132358627723435031s, LSN: 21, GlobalCommittedLsn: 21, PartitionKeyRangeId: , IsValid: True, StatusCode: 404, SubStatusCode: 0, RequestCharge: 1, ItemLSN: -1, SessionToken: -1#21, UsingLocalLSN: False, TransportException: null, ResourceType: Database, OperationType: Read
ResponseTime: 2020-06-07T11:49:26.7346281Z, StoreResult: StorePhysicalAddress: rntbd://10.0.0.24:11000/apps/4d851b1c-a3a8-4e9f-8f64-eb20a41298cd/services/72192d2e-f151-46b6-b166-d40d65173948/partitions/13f2ad2f-6b39-45c4-a052-c3fca79a7a69/replicas/132358808624984341s, LSN: 21, GlobalCommittedLsn: 21, PartitionKeyRangeId: , IsValid: True, StatusCode: 404, SubStatusCode: 0, RequestCharge: 1, ItemLSN: -1, SessionToken: -1#21, UsingLocalLSN: False, TransportException: null, ResourceType: Database, OperationType: Read
, SDK: Microsoft.Azure.Documents.Common/2.11.0
at /Users/yujimasaoka/sources/nodejs/cosmosdb/azure-cosmosdb-sdk-check/node_modules/@azure/cosmos/dist/index.js:6973:39
at Generator.next (<anonymous>)
at fulfilled (/Users/yujimasaoka/sources/nodejs/cosmosdb/azure-cosmosdb-sdk-check/node_modules/tslib/tslib.js:112:62)
at processTicksAndRejections (internal/process/task_queues.js:97:5) {
code: 404,
body: {
code: 'NotFound',
message: 'Message: {"Errors":["Resource Not Found"]}\r\n' +
'ActivityId: 93b991e0-f761-4ceb-8028-e076813b0f49, Request URI: /apps/4d851b1c-a3a8-4e9f-8f64-eb20a41298cd/services/72192d2e-f151-46b6-b166-d40d65173948/partitions/13f2ad2f-6b39-45c4-a052-c3fca79a7a69/replicas/132358627723435031s, RequestStats: \r\n' +
'RequestStartTime: 2020-06-07T11:49:26.7346281Z, RequestEndTime: 2020-06-07T11:49:26.7346281Z, Number of regions attempted:1\r\n' +
'ResponseTime: 2020-06-07T11:49:26.7346281Z, StoreResult: StorePhysicalAddress: rntbd://10.0.0.29:11300/apps/4d851b1c-a3a8-4e9f-8f64-eb20a41298cd/services/72192d2e-f151-46b6-b166-d40d65173948/partitions/13f2ad2f-6b39-45c4-a052-c3fca79a7a69/replicas/132358627723435031s, LSN: 21, GlobalCommittedLsn: 21, PartitionKeyRangeId: , IsValid: True, StatusCode: 404, SubStatusCode: 0, RequestCharge: 1, ItemLSN: -1, SessionToken: -1#21, UsingLocalLSN: False, TransportException: null, ResourceType: Database, OperationType: Read\r\n' +
'ResponseTime: 2020-06-07T11:49:26.7346281Z, StoreResult: StorePhysicalAddress: rntbd://10.0.0.24:11000/apps/4d851b1c-a3a8-4e9f-8f64-eb20a41298cd/services/72192d2e-f151-46b6-b166-d40d65173948/partitions/13f2ad2f-6b39-45c4-a052-c3fca79a7a69/replicas/132358808624984341s, LSN: 21, GlobalCommittedLsn: 21, PartitionKeyRangeId: , IsValid: True, StatusCode: 404, SubStatusCode: 0, RequestCharge: 1, ItemLSN: -1, SessionToken: -1#21, UsingLocalLSN: False, TransportException: null, ResourceType: Database, OperationType: Read\r\n' +
', SDK: Microsoft.Azure.Documents.Common/2.11.0'
},
headers: {
'content-type': 'application/json',
date: 'Sun, 07 Jun 2020 11:49:25 GMT',
lsn: '21',
server: 'Microsoft-HTTPAPI/2.0',
'strict-transport-security': 'max-age=31536000',
'transfer-encoding': 'chunked',
'x-ms-activity-id': '93b991e0-f761-4ceb-8028-e076813b0f49',
'x-ms-cosmos-llsn': '21',
'x-ms-gatewayversion': 'version=2.11.0',
'x-ms-global-committed-lsn': '21',
'x-ms-last-state-change-utc': 'Sat, 06 Jun 2020 01:16:08.289 GMT',
'x-ms-number-of-read-regions': '0',
'x-ms-request-charge': '2',
'x-ms-schemaversion': '1.9',
'x-ms-serviceversion': 'version=2.11.0.0',
'x-ms-session-token': '0:-1#21',
'x-ms-transport-request-id': '230761',
'x-ms-xp-role': '2',
'x-ms-throttle-retry-count': 0,
'x-ms-throttle-retry-wait-time-ms': 0
},
activityId: '93b991e0-f761-4ceb-8028-e076813b0f49'
}
と言った形でエラーが返ってきますね。Resource Not Found が出力されています。
では、メソッド内にあったコメントは何だったのか、というと、(恐らくですが)実はこのcreateIfNotExists
メソッドは、Databases クラスにも存在しています。
Databases クラスで作ったcreateIfNotExists
メソッドをそのままコピペしたままにしてしまったため、発生してしまったゴミコメントだと推測されます。
public async createIfNotExists(
body: DatabaseRequest,
options?: RequestOptions
): Promise<DatabaseResponse> {
if (!body || body.id === null || body.id === undefined) {
throw new Error("body parameter must be an object with an id property");
}
/*
1. Attempt to read the Database (based on an assumption that most databases will already exist, so its faster)
2. If it fails with NotFound error, attempt to create the db. Else, return the read results.
*/
try {
const readResponse = await this.client.database(body.id).read(options);
return readResponse;
} catch (err) {
if (err.code === StatusCodes.NotFound) {
const createResponse = await this.create(body, options);
// Must merge the headers to capture RU costskaty
mergeHeaders(createResponse.headers, err.headers);
return createResponse;
} else {
throw err;
}
}
}
いましたね〜!!そっくりそのままで、あるあるなやつです。(笑)
Containers クラスと違うのは、try-catch の最初にある
const readResponse = this.database.container(body.id).read(options)
これが
const readResponse = await this.client.database(body.id).read(options);
となっているぐらいでしょうか。
と言うわけで、こういった些細なことでもフィードバックしよう、ということでPR
を作成してみました。
さいごに
今回は、Azure Cosmos DB に JavaScript/Node.js で接続する際に Database クラスを使って生成する Container クラスについて、 初期化時まわりの動作について確認してみました。
Container クラス vs Containers クラス
の関係について、理解いただけたでしょうか。
終盤、少し脱線をしてしまいましたが、次回は、Container
クラス内で行われる CRUD 処理のメソッドについてみていきたいと思います。
ちなみに、今回のように中身を見る=他人が書いたコードをレビューする という行為でもあります。そのため、前回のように自身にとって勉強になる時もあれば、今回のように、逆にミスを見つけたりすることもあります。
もし、ミスを見つけた場合は、優しい心を持って、ぜひ開発者にフィードバックしてあげてください。