【Cosmos DBメモ】Cosmos DBで日付項目をパーティション キーに使用したい場合

# はじめに
Azure Cosmos DB の学習中にパーティション キーに日付項目を使用するケースがあり、これが一筋縄ではいかず、調査・検討した結果をメモとして記載しました。
見解に誤りやより良い方法があればコメント・アドバス願います。

## Cosmos DBへの入出力操作の参考
* Azure Functions 2.x 以降に対する Azure Cosmos DB の入力バインド
  * https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-cosmosdb-v2-input?tabs=csharp
* Azure Functions 2.x 以降に対する Azure Cosmos DB の出力バインド
  * https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-cosmosdb-v2-output?tabs=csharp

# 事前知識
## パーティション キーの役割
Azure Cosmos DB は、データベースの個別のコンテナーをパーティション分割を使用した水平スケーリングすることで、数ミリ秒 (1桁台) の応答時間と、自動および即時のスケーラビリティを実現しています。
そして、パーティション キーはコンテナーをパーティション分割する際のキーであり、パーティション キーに指定した項目の値が同じデータは同じパーティションに格納されます。
なお、パーティション キーはコンテナー作成時のみ指定可能で、後から変更することはできません。

## パーティション キーのルール（「Microsof Docs」より）
* パーティション キーを選択した後は、その場で変更することはできません。 パーティション キーを変更する必要がある場合は、新しい必要なパーティション キーを使用して、新しいコンテナーにデータを移動する必要があります。
* すべての コンテナーで、パーティション キーが次のようになっている必要があります。
  * 値が変更されないプロパティであること。 プロパティがパーティション キーの場合、そのプロパティの値を更新することはできません。
  * 高いカーディナリティがあること。 言い換えると、プロパティには、有効な値が広範囲に及ぶことが必要です。
  * 要求ユニット (RU) の消費量とデータ ストレージをすべての論理パーティションに均等に分散すること。 これにより、物理パーティション全体でも RU の消費とストレージの分散が保証されます。

### パーティション キーのパス (例: "/userId")のルール
* パーティション キーのパスには、英数字とアンダースコア (_) 文字を使用できます。 また、標準パス表記 (/) を使用して、入れ子になったオブジェクトを使用することもできます。

### パーティション キーの値 (例:"Andrew")のルール
* パーティション キーの値には、文字列型または数値型を使用できます。
* パーティション キー値の最大長は2048 バイトです。

## 参考
* Azure Cosmos DB でのパーティション分割と水平スケーリング
  * https://docs.microsoft.com/ja-jp/azure/cosmos-db/partitioning-overview
* Azure Cosmos DB サービスのクォータ
  * https://docs.microsoft.com/ja-jp/azure/cosmos-db/concepts-limits


## Azure Cosmos DB での日付の扱い
Azure Cosmos DB はJSON ドキュメントとしてモデル化されて保存しており、少数の基本的な型 (String、Number、Boolean、Array、Object、Null) のみサポートし、DateTime 型は直接サポートされません。
そのため、DateTime を文字列として格納する必要があります。

また、現在、Azure Cosmos DB では、日付のローカリゼーションはサポートされていないため、Microsof は日付は UTC の DateTime 値を ISO 8601 UTC 標準に準拠した `yyyy-MM-ddTHH:mm:ss.fffffffZ` 形式の文字列で保存することを推奨しています。

なお、フィルターとして DateTime 文字列を使用する範囲クエリは、DateTime 文字列がすべて UTC であり、同じ長さである場合にのみサポートされます。

## 参考
* Azure Cosmos DB で日付を扱う
  * https://docs.microsoft.com/ja-jp/azure/cosmos-db/sql/working-with-dates


# 日付項目をパーティション キーに使用したい場合
## 方法１（王道パターンだと思いきやNG）
UTC の DateTime 値に変換した日付を ISO 8601 UTC 標準に準拠した `yyyy-MM-ddTHH:mm:ss.fffffffZ` 形式の文字列で保存する。

### 概要
DateTime 型の日付項目を以下の書式で文字列に変換
* 書式： `"yyyy-MM-dd'T'HH:mm:ss'Z'"`
  * 実装イメージ

  ```cs
  [JsonProperty("datePartitionKey")]
  public string DatePartitionKey => {DateTime 型の項目}.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'");
  ```

* 書式： `"O"`
  * 実装イメージ
  
  ```cs
  [JsonProperty("datePartitionKey")]
  public string DatePartitionKey => {DateTime 型の項目}.ToUniversalTime().ToString("O");
  ```

### 課題・問題点など
* 「PartitionKey extracted from document doesn't match the one specified in the header.」のエラーで保存できず。

### 参考
* 標準の日時書式指定文字列
  * https://docs.microsoft.com/ja-jp/dotnet/standard/base-types/standard-date-and-time-format-strings
* Azure Cosmos DB での無効な要求例外の診断とトラブルシューティング
  * https://docs.microsoft.com/ja-jp/azure/cosmos-db/sql/troubleshoot-bad-request

## 方法２
UTC の DateTime 値に変換した日付を Unix タイムスタンプとして保存する。
※Azure Cosmos DB の内部 Timestamp（_ts）プロパティが採用

### 概要
UnixDateTimeConverter クラスを使用して、DateTime を数値としてシリアル化

* 実装イメージ

```cs
  [JsonProperty("datePartitionKey")]
  [JsonConverter(typeof(Newtonsoft.Json.Converters.UnixDateTimeConverter))]
  public string DatePartitionKey => {DateTime 型の項目}.ToUniversalTime();
```

### 参考
* UnixDateTimeConverter Class
  * https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_Converters_UnixDateTimeConverter.htm

### 課題・問題点など
* 日付項目に対する検索条件も正常に機能し、特に問題点は見当たらない。

## 方法３
日付を `yyyy-MM-dd` 形式の文字列で保存する。
※国際化が不要な簡易なシステムであれば、UTC 変換も不要でJSONを参照した際も分かりやすい。

### 概要
DateTime 型の日付項目を日付のみの書式で文字列に変換

* 実装イメージ

```cs
  [JsonProperty("datePartitionKey")]
  public string DatePartitionKey => {DateTime 型の項目}.ToString("yyyy-MM-dd");
```

### 参考
* 標準の日時書式指定文字列
  * https://docs.microsoft.com/ja-jp/dotnet/standard/base-types/standard-date-and-time-format-strings

### 課題・問題点など
* 日付項目に対する検索条件用に別途、DateTime 型の項目が必要。


# まとめ
* 日付項目をパーティション キーに使用する場合、日付項目をUTCに変換した値を Unix タイムスタンプ に変換した数値で保存する[「方法２」](#方法２)が良さそうである。
* ただし、国際化が不要な簡易なシステムであれば、日付項目を日付のみの書式で文字列で保存する[「方法３」](#方法３)が、UTC 変換も不要でJSONを参照した際も分かりやすいため、検討の余地はあると考えます。