🌕

【MS Learn】Azure Cosmos DB for NoSQL を使用して .NET アプリを構築する をやってみた

2024/01/05に公開

Learnの通りにやればOKな感じです。今回は割とすんなりできました。
文章はご本家を読みましょう(適当)
コンソールやコードを触る部分のみ抜き出して書いてます。

https://learn.microsoft.com/ja-jp/training/modules/build-dotnet-app-azure-cosmos-db-nosql/

環境

GitHub Codespaces
.NET 8

ユニット2: 準備

Azure Cosmos DB for NoSQL アカウントを作成する

サンドボックスが有効化されたので、そのAzure Cloud Shellで実行します。

サンドボックスが作成したリソースグループにCosmosDBのアカウントを作成します。NoSQL用APIです。※リソースグループは指定されているので、そのままコピーすればよいです。

Azure Cloud Shell
$ let suffix=$RANDOM*$RANDOM

$ az cosmosdb create --resource-group "learn-b6dbb368-2dab-432f-a913-7a9f90d40ffd" --name "mslearn-$suffix" --locations "regionName=westus"

作成には少し時間がかかります。

アカウントの接続文字列を取得する

  1. NoSQL 用 API アカウントの名前を取得します。
Azure Cloud Shell
$ az cosmosdb list --resource-group "learn-b6dbb368-2dab-432f-a913-7a9f90d40ffd" --query "sort_by([].{name:name,created:systemData.createdAt}, &created)" --output table
Name               Created
-----------------  --------------------------------
mslearn-335598080  2024-01-05T07:59:28.547985+00:00
  1. 接続文字列を取得します。
Azure Cloud Shell
$ az cosmosdb keys list --resource-group "learn-b6dbb368-2dab-432f-a913-7a9f90d40ffd" --name $(az cosmosdb list --resource-group "learn-b6dbb368-2dab-432f-a913-7a9f90d40ffd" --query "sort_by([].{name:name,created:systemData.createdAt}, &created)[0].name" --output tsv) --type connection-strings --query "connectionStrings[?description=='Primary SQL Connection String'].connectionString" --output tsv

AccountEndpoint=https://mslearn-335598080.documents.azure.com:443/;AccountKey=******nD5MX7HU8XIa34dFj8Z4hEdTb3knCj6EqOKl******ZM3eZcyqbhVv4W0cXOT0JP******ACDbIl1r5g==;

この接続文字列は次のユニットで使用します。大事。

ユニット3: 演習 - .NET アプリと参照 SDK を作成する

タイトルの日本語が若干おかしいですが。

ここでやることは3つ。

  • .NET プロジェクトを作成する
  • SDK へのパッケージ参照を追加する
  • クライアント クラスを使って接続する

それではやっていきます。

.NET コンソール プロジェクトを作成する

  1. 任意のディレクトリに新しいコンソール アプリケーションを作成します。
$ dotnet new console
  1. NuGet から Microsoft.Azure.Cosmos SDK のバージョン 3 へのパッケージ参照を追加します。
$ dotnet add package Microsoft.Azure.Cosmos --version 3.*
  1. .NET プロジェクトをビルドします。
$ dotnet build
MSBuild version 17.8.0+6cdef4241 for .NET
  Determining projects to restore...
  All projects are up-to-date for restore.
/usr/share/dotnet/sdk/8.0.100-rc.2.23502.2/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.RuntimeIdentifierInference.targets(311,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [/workspaces/cosmos-db-dotnet/cosmos-db-dotnet.csproj]
  cosmos-db-dotnet -> /workspaces/cosmos-db-dotnet/bin/Debug/net8.0/cosmos-db-dotnet.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:06.06

アカウントに接続する

1-3. Program.cs のコードを削除して、以下を追加します。

Program.cs
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Fluent;
using Microsoft.Azure.Cosmos.Linq;
  1. connectionString という名前の定数文字列変数を作成します。先ほど取得した接続文字列を貼り付けます。
Program.cs
const string connectionString = "<自分の環境のキー>";
  1. コンソールに接続文字列を出力します。
Program.cs
Console.WriteLine($"[Connection string]:\t{connectionString}");
  1. serializerOptions という名前の CosmosSerializationOptions クラスの新しいインスタンスを作成します。 PropertyNamingPolicy プロパティを、CamelCase 列挙型の値 CamelCase に設定します。あ、はい。
Program.cs
CosmosSerializationOptions serializerOptions = new()
{
    PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
};
  1. 接続文字列をコンストラクターに渡して、CosmosClientBuilder クラスの新しいインスタンスを作成します。 次に、WithSerializerOptions(CosmosSerializationOptions) fluent メソッドをつなげて、このメソッドのパラメーターを serializerOptions に設定します。 Build() メソッドをつなげ、CosmosClient 型で client という名前のインスタンスを作成します。 最後に、クライアント変数の作成を using ステートメントでラップします。
    めちゃくちゃ親切に解説してくれてる。
Program.cs
using CosmosClient client = new CosmosClientBuilder(connectionString)
    .WithSerializerOptions(serializerOptions)
    .Build();
  1. クライアントの準備ができていることを示すメッセージを出力します。
Program.cs
Console.WriteLine("[Client ready]");

ソースの全体はこうなります。

Program.cs
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Fluent;
using Microsoft.Azure.Cosmos.Linq;

const string connectionString = "AccountEndpoint=https://mslearn-335598080.documents.azure.com:443/;AccountKey=******nD5MX7HU8XIa34dFj8Z4hEdTb3knCj6EqOKl******ZM3eZcyqbhVv4W0cXOT0JP******ACDbIl1r5g==;";

Console.WriteLine($"[Connection string]:\t{connectionString}");

CosmosSerializationOptions serializerOptions = new()
{
    PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
};

using CosmosClient client = new CosmosClientBuilder(connectionString)
    .WithSerializerOptions(serializerOptions)
    .Build();

Console.WriteLine("[Client ready]");

作業を確認

  1. ターミナルで .NET アプリケーションを実行する
dotnet run
  1. アプリケーションを実行した出力を確認します。
[Connection string]:    AccountEndpoint=https://mslearn-335598080.documents.azure.com:443/;AccountKey=******nD5MX7HU8XIa34dFj8Z4hEdTb3knCj6EqOKl******ZM3eZcyqbhVv4W0cXOT0JP******ACDbIl1r5g==;
[Client ready]
  • コードのレビュー
    *.csproj ファイルは .NET8 を使っているので以下のようになりました。
*.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>cosmos_db_dotnet</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.*" />
  </ItemGroup>

</Project>

ユニット4: 演習 - API for NoSQL アカウントとリソースを作成する

この演習では、cosmicworks という名前のデータベースと、products という名前のシングル コンテナーを作成します。

ここでの重要な要件は次のとおりです。

  1. データベースがまだ存在しない場合は作成する
  2. コンテナーがまだ存在しない場合は作成する

データベースを作成する

  1. Program.cs を開きます。
  2. "cosmicworks" という新しいデータベースを作成または取得します。
Program.cs
Database database = await client.CreateDatabaseIfNotExistsAsync(
    id: "cosmicworks"
);
  1. データベースのIDを出力します。
Program.cs
Console.WriteLine($"[Database created]:\t{database.Id}");

Program.cs の保存を忘れずに。

コンテナーを作成する

  1. 新しいコンテナー "products" を作成します。パーティションキーは "/categoryId" とします。
Program.cs
ContainerProperties properties = new(
    id: "products",
    partitionKeyPath: "/categoryId"
);
  1. 自動スケーリング スループット オブジェクトを作成します。
Program.cs
var throughput = ThroughputProperties.CreateAutoscaleThroughput(
    autoscaleMaxThroughput: 1000
);
  1. 新しいコンテナを作成、存在する場合は取得します。
Program.cs
Container container = await database.CreateContainerIfNotExistsAsync(
    containerProperties: properties,
    throughputProperties: throughput
);
  1. コンテナーのIDを出力します。
Program.cs
Console.WriteLine($"[Container created]:\t{container.Id}");

Program.cs の保存を忘れずに。

項目のレコード型を作成する

  1. Item.cs という名前の新しいファイルを作成します。
  2. このコンテナーのすべての項目で使用する、3 つのプロパティ (id、categoryId、type) を含む Item という名前の基本レコード型を作成します。
Items.cs
public record Item(
    string Id,
    string CategoryId,
    string Type
);
  1. Items.cs を保存します。
  2. Category.cs という名前のファイルを別に新規作成します。
  3. Item 型を継承する Category という名前の新しい型を作成します。
Category.cs
public record Category(
    string Id,
    string CategoryId
) : Item(
    Id,
    CategoryId,
    nameof(Category)
);
  1. Category.cs ファイルを保存します。
  2. Product.cs という名前のファイルを作成します。
  3. Item を継承する Product という名前の新しい型を作成し、いくつかの新しいプロパティ (name、price、archived、quantity) を追加します。
public record Product(
    string Id,
    string CategoryId
) : Item(
    Id,
    CategoryId,
    nameof(Product)
)
{
    public string Name { get; init; } = default!;
    public decimal Price { get; init; }
    public bool Archived { get; init; }
    public int Quantity { get; init; }
};
  1. Product.cs ファイルを保存します。

作業を確認

ターミナルで .NET アプリケーションを実行すると以下のような出力があれば成功です。

$ dotnet run
---
[Database created]:     cosmicworks
[Container created]:    products

ユニット5: 演習 - 新しい項目を作成する

このアプリケーションでは、次の 2 つのケースを処理します。

  • カテゴリが空の場合は、そのカテゴリの項目を個別に作成するだけで問題ありません。 作成する関連製品項目はありません。
  • 一方、カテゴリに関連製品が含まれる場合、カテゴリ項目とそれに関連する製品項目を同時に作成する必要があります。

ここでの重要な要件は次の 2 つです。

  1. 1 つの操作として項目を個別に作成する
  2. トランザクション バッチを使用して複数の関連項目を作成する

コンテナーに個々の項目を追加する

  1. Program.cs を開きます。
  2. goggles という名前の新しい Category インスタンスを作成します。
Program.cs
Category goggles = new(
    Id: "ef7fa0f1-0e9d-4435-aaaf-a778179a94ad",
    CategoryId: "gear-snow-goggles"
);
  1. 前に作成した Category インスタンスの categoryId プロパティと同じ値を使って、新しい PartitionKey インスタンスを作成します。
Program.cs
PartitionKey gogglesKey = new("gear-snow-goggles");
  1. 作成する項目のオブジェクトとパーティション キー値を渡して、項目を作成または置換します。
Program.cs
Category result = await container.UpsertItemAsync(goggles, gogglesKey);
  1. 項目の一意識別子や項目の種類などを出力します。
Program.cs
Console.WriteLine($"[New item created]:\t{result.Id}\t(Type: {result.Type})");
  1. helmets という名前の新しい Category インスタンスを作成します。
Program.cs
Category helmets = new(
    Id: "91f79374-8611-4505-9c28-3bbbf1aa7df7",
    CategoryId: "gear-climb-helmets"
);
  1. goggles と同じようにPartitionKey インスタンスを作成します。
Program.cs
PartitionKey helmetsKey = new("gear-climb-helmets");
  1. 項目を作成または置換します。
Program.cs
ItemResponse<Category> response = await container.UpsertItemAsync(helmets, helmetsKey);
  1. 基になる項目の一意識別子、基になる項目の種類、RU での要求使用量など出力します。
Program.cs
Console.WriteLine($"[New item created]:\t{response.Resource.Id}\t(Type: {response.Resource.Type})\t(RUs: {response.RequestCharge})");
  1. Program.cs ファイルを保存します。

複数の操作をトランザクション バッチとして実装する

RDBでいうところのトランザクションと同じような感じで、カテゴリと製品を1つのトランザクションで保存します。

  1. Program.cs で tents という名前の新しい Category インスタンスを作成します。
Program.cs
Category tents = new(
    Id: "5df21ec5-813c-423e-9ee9-1a2aaead0be4",
    CategoryId: "gear-camp-tents"
);
  1. Product 型の 4 つのインスタンスを作成します。
Program.cs
Product cirroa = new(
    Id: "e8dddee4-9f43-4d15-9b08-0d7f36adcac8",
    CategoryId: "gear-camp-tents"
){
    Name = "Cirroa Tent",
    Price = 490.00m,
    Archived = false,
    Quantity = 15
};

Product kuloar = new(
    Id: "e6f87b8d-8cd7-4ade-a005-14d3e2fbd1aa",
    CategoryId: "gear-camp-tents"
){
    Name = "Kuloar Tent",
    Price = 530.00m,
    Archived = false,
    Quantity = 8
};

Product mammatin = new(
    Id: "f7653468-c4b8-47c9-97ff-451ee55f4fd5",
    CategoryId: "gear-camp-tents"
){
    Name = "Mammatin Tent",
    Price = 0.00m,
    Archived = true,
    Quantity = 0
};

Product nimbolo = new(
    Id: "6e3b7275-57d4-4418-914d-14d1baca0979",
    CategoryId: "gear-camp-tents"
){
    Name = "Nimbolo Tent",
    Price = 330.00m,
    Archived = false,
    Quantity = 35
};
  1. gear-camp-tents という PartitionKey インスタンスを作成します。
Program.cs
PartitionKey tentsKey = new("gear-camp-tents");
  1. 新しいトランザクション バッチを作成します。
Program.cs
TransactionalBatch batch = container.CreateTransactionalBatch(tentsKey)
    .UpsertItem<Category>(tents)
    .UpsertItem<Product>(cirroa)
    .UpsertItem<Product>(kuloar)
    .UpsertItem<Product>(mammatin)
    .UpsertItem<Product>(nimbolo);
  1. バッチ操作を開始していることを示すメッセージをコンソールに出力します。
Program.cs
Console.WriteLine("[Batch started]");
  1. バッチを実行します。
Program.cs
using TransactionalBatchResponse batchResponse = await batch.ExecuteAsync();
  1. for ループを使って、応答内のすべての項目を反復処理します。
Program.cs
for (int i = 0; i < batchResponse.Count; i++)
{
    TransactionalBatchOperationResult<Item> batchResult = batchResponse.GetOperationResultAtIndex<Item>(i);
    Console.WriteLine($"[New item created]:\t{batchResult.Resource.Id}\t(Type: {batchResult.Resource.Type})");
}
  1. バッチが完了したことを示す別のメッセージを出力します。
Program.cs
Console.WriteLine($"[Batch completed]:\t(RUs: {batchResponse.RequestCharge})");
  1. Program.cs を保存します。

作業を確認

.NET アプリケーションを実行します。出力を確認します。

$ dotnet run
---
[New item created]:     ef7fa0f1-0e9d-4435-aaaf-a778179a94ad    (Type: Category)
[New item created]:     91f79374-8611-4505-9c28-3bbbf1aa7df7    (Type: Category)        (RUs: 6.29)
[Batch started]
[New item created]:     5df21ec5-813c-423e-9ee9-1a2aaead0be4    (Type: Category)
[New item created]:     e8dddee4-9f43-4d15-9b08-0d7f36adcac8    (Type: Product)
[New item created]:     e6f87b8d-8cd7-4ade-a005-14d3e2fbd1aa    (Type: Product)
[New item created]:     f7653468-c4b8-47c9-97ff-451ee55f4fd5    (Type: Product)
[New item created]:     6e3b7275-57d4-4418-914d-14d1baca0979    (Type: Product)
[Batch completed]:      (RUs: 36.76)

ユニット6: 演習 - 項目の読み取りとクエリ実行

いわゆる検索ですね。

ここでの重要な要件は次の 3 つです。

  1. 一意識別子とパーティション キー値の両方を使って項目のポイント読み取りを行う
  2. シンプルなクエリ文字列を使用してクエリを作成する
  3. フィード反復子を使用してクエリの結果を改ページする

項目のポイント読み取り

  1. Program.cs を開きます。
  2. gear-climb-helmets という PartitionKey インスタンスを作成します。
Program.cs
PartitionKey readKey = new("gear-climb-helmets");
  1. 特定の項目の読み取りを行います。
Program.cs
ItemResponse<Category> readResponse = await container.ReadItemAsync<Category>(
    id: "91f79374-8611-4505-9c28-3bbbf1aa7df7",
    partitionKey: readKey
);
  1. レスポンスを読み取ります。
Program.cs
Category readItem = readResponse.Resource;
  1. 読み取り操作の一意識別子と要求料金を出力します。
Program.cs
Console.WriteLine($"[Point read item]:\t{readItem.Id}\t(RUs: {readResponse.RequestCharge})");
  1. Program.cs を保存します。

クエリを実行する

  1. Program.cs で、クエリ文字列を作成します。 ただし、categoryId フィルターに @partitionKey という名前のパラメーターを使用します。
Program.cs
string statement = "SELECT * FROM products p WHERE p.categoryId = @partitionKey";
  1. クエリ文字列を使用し、QueryDefinition クラスの新しいインスタンスを作成します。
Program.cs
var query = new QueryDefinition(
    query: statement
);
  1. @partitionKey パラメーターに gear-camp-tents 値を代入します。
Program.cs
var parameterizedQuery = query.WithParameter("@partitionKey", "gear-camp-tents");
  1. 特定のクエリの反復子を取得します。
Program.cs
using FeedIterator<Product> feed = container.GetItemQueryIterator<Product>(
    queryDefinition: parameterizedQuery
);
  1. クエリを書き込みます。
Program.cs
Console.WriteLine($"[Start query]:\t{statement}");

クエリ結果をページ分割する

ここはバラバラだとちょっとわかりづらいのでまとめます。

Program.cs
// 1. 要求料金の合計を保持する変数
double totalRequestCharge = 0d;
// 2. 結果をループ
while (feed.HasMoreResults)
{
    // 3. 結果の新しいページを取得します。
    FeedResponse<Product> page = await feed.ReadNextAsync();
    // 4.  要求料金の合計をインクリメント
    totalRequestCharge += page.RequestCharge;
    // 5. ページ内の実際の項目をループして取得します。
    foreach (Product item in page)
    {
        // 6. 返された項目の id プロパティと name プロパティを書き込みます。
        Console.WriteLine($"[Returned item]:\t{item.Id}\t(Name: {item.Name ?? "N/A"})");
    }
}
// 7. 計算した要求料金の合計を書き込みます。
Console.WriteLine($"[Query metrics]:\t(RUs: {totalRequestCharge})");
  1. Program.cs を保存します。

作業を確認

アプリケーションを実行して、出力を確認します。

$ dotnet run
---
[Point read item]:      91f79374-8611-4505-9c28-3bbbf1aa7df7    (RUs: 1)
[Start query]:  SELECT * FROM products p WHERE p.categoryId = @partitionKey
[Returned item]:        5df21ec5-813c-423e-9ee9-1a2aaead0be4    (Name: N/A)
[Returned item]:        e8dddee4-9f43-4d15-9b08-0d7f36adcac8    (Name: Cirroa Tent)
[Returned item]:        e6f87b8d-8cd7-4ade-a005-14d3e2fbd1aa    (Name: Kuloar Tent)
[Returned item]:        f7653468-c4b8-47c9-97ff-451ee55f4fd5    (Name: Mammatin Tent)
[Returned item]:        6e3b7275-57d4-4418-914d-14d1baca0979    (Name: Nimbolo Tent)
[Query metrics]:        (RUs: 2.94)

ユニット7: 演習 - 統合言語クエリを使用して項目を列挙する (LINQ)

統合言語クエリ (LINQ) 構文を使って、データのコレクションのクエリを実行します。

ここでの重要な要件は次の 2 つです。

  1. LINQ 構文を使って新しいクエリを作成する
  2. クエリをフィード反復子に変換して結果を取得する

LINQ 式を使用してクエリを実行する

  1. Program.cs を開きます。
  2. 新しい LINQ クエリを作成します。
Program.cs
IOrderedQueryable<Product> queryable = container.GetItemLinqQueryable<Product>();
  1. Where メソッドと OrderBy メソッドを使って LINQ 式を作成し、新しい変数に式を格納します。
Program.cs
var matches = queryable
    .Where(p => p.Type == nameof(Product))
    .Where(p => !p.Archived)
    .OrderBy(p => p.Price);
  1. LINQ 式からフィード反復子を取得します。
Program.cs
using FeedIterator<Product> linqFeed = matches.ToFeedIterator();
  1. コンソールにメッセージを出力します。
Program.cs
Console.WriteLine($"[Start LINQ query]");
  1. Program.cs を保存します。

LINQ クエリの結果をページ分割する

ここも同じくまとめます。

Program.cs
// 1. ページがなくなるまで処理を繰り返す
while (linqFeed.HasMoreResults)
{
    // 2. 結果のページを取得
    FeedResponse<Product> page = await linqFeed.ReadNextAsync();
    // 3. 現在のページの要求使用量を出力
    Console.WriteLine($"[Page RU charge]:\t{page.RequestCharge}");
    // 4. ページの項目を反復処理
    foreach (Product item in page)
    {
        // 個々の項目をコンソールに表示
        Console.WriteLine($"[Returned item]:\t{item}");
    }
}
  1. Program.cs を保存します。

作業を確認

アプリケーションを実行して結果を確認します。

$ dotnet run
---
[Start LINQ query]
[Page RU charge]:       3
[Returned item]:        Product { Id = 6e3b7275-57d4-4418-914d-14d1baca0979, CategoryId = gear-camp-tents, Type = Product, Name = Nimbolo Tent, Price = 330, Archived = False, Quantity = 35 }
[Returned item]:        Product { Id = e8dddee4-9f43-4d15-9b08-0d7f36adcac8, CategoryId = gear-camp-tents, Type = Product, Name = Cirroa Tent, Price = 490, Archived = False, Quantity = 15 }
[Returned item]:        Product { Id = e6f87b8d-8cd7-4ade-a005-14d3e2fbd1aa, CategoryId = gear-camp-tents, Type = Product, Name = Kuloar Tent, Price = 530, Archived = False, Quantity = 8 }

おわりに

GitHub Codespaces便利!!

Discussion