Alternative Architecture DOJO

オルターブースのクラウドネイティブ特化型ブログです。

Azure Cosmos DBデザインパターン解説:Event sourcingパターン

こんにちは、MLBお兄さんこと松村です。
ついに MLB が開幕しました。今年はどのようなシーズンとなるでしょうか。


今回は Azure Cosmos DB for NoSQL のデザインパターン解説記事の第6弾です。
前回は「ドキュメントのバージョン管理 (Document versioning) パターン」を解説しました。

aadojo.alterbooth.com

今回は「イベントソーシング (Event sourcing) パターン」について解説をします。
このデザインパターンについては、Cosmos DB の GitHub リポジトリやブログで紹介されていますので、詳しく読みたい方はこちらもご覧ください。

github.com

devblogs.microsoft.com]


イベントソーシングパターンは、サーバーレスアーキテクチャーに関連してよく聞くデザインパターンです。
まずイベントソーシングパターンは、データに関するイベントをすべて追跡可能な状態で記録する方法です。
データに関するイベントとして、CRUD のうちの以下を指します。

  • 登録 (create)
  • 更新 (update)
  • 削除 (delete)

また、イベントの記録と同時に、そのイベントの対象となるデータ自体も一緒に記録します。
こうすることで、以下のようなメリットがあります。前回の Document versioning パターンにも近いものがあります。

  • データの変化を時系列で追跡できるため、監査証跡に用いられる
  • システムがクラッシュして、当該データが記録されていない場合でも、イベントデータを再送することができる(やり直せる)
  • 大量のデータを効率的に処理することができ、スケーラビリティを保つことができる
  • テストのためにイベントを複製することができる

サンプルコードと照らし合わせながら、理解していきましょう。
サンプルはデザインパターンのリポジトリで公開されています。リポジトリをクローンして、実際に動かしてみましょう。

サンプルコードでは、 EC サイトのショッピングカートに関するイベントを記録するようになっています。
イベントは4種類定義されています。

string[] actions = new string[]
{
    "cart_created",     // カートの作成
    "product_added",    // カートへ商品追加
    "product_deleted",  // カートから商品削除
    "cart_purchased"    // カート内の商品購入
};

イベントごとに「いつ・誰が・どこへ・どのようなデータ」を更新操作したのか、というのを随時記録していきます。
イベントごとに記録されるデータは以下のような形になっています。

カートの作成

{
    "id": "a6e8d56b-4ffb-4a01-a6c1-105955ff70fc",
    "CartId": "d5e3f139-fafe-4acd-91cf-0fbab0e12584",
    "SessionId": "36b8ed87-0a5e-4b80-aa81-926bbed564ec",
    "UserId": 339,
    "EventType": "cart_created",
    "Product": null,
    "QuantityChange": null,
    "ProductsInCart": null,
    "EventTimestamp": "2024/03/27 14:27:13",
    ...
}

カートへの商品追加

{
    "id": "107ad5b5-eb9d-4263-b7ed-cab34542f979",
    "CartId": "d5e3f139-fafe-4acd-91cf-0fbab0e12584",
    "SessionId": "36b8ed87-0a5e-4b80-aa81-926bbed564ec",
    "UserId": 339,
    "EventType": "product_added",
    "Product": "Product 1",
    "QuantityChange": 0,
    "ProductsInCart": [
        {
            "ProductName": "Product 1",
            "Quantity": 0
        }
    ],
    "EventTimestamp": "2024/03/27 14:27:13",
    ...
}

カートからの商品削除

{
    "id": "07cf958e-b08b-485b-a204-e7ab91b42ca0",
    "CartId": "d5e3f139-fafe-4acd-91cf-0fbab0e12584",
    "SessionId": "36b8ed87-0a5e-4b80-aa81-926bbed564ec",
    "UserId": 339,
    "EventType": "product_deleted",
    "Product": "Product 2",
    "QuantityChange": -1,
    "ProductsInCart": [
        {
            "ProductName": "Product 1",
            "Quantity": 0
        }
    ],
    "EventTimestamp": "2024/03/27 14:27:13",
    ...
}

カート内の商品購入

{
    "id": "edd4eda9-fdd1-428a-8e49-84839cbd1f54",
    "CartId": "d5e3f139-fafe-4acd-91cf-0fbab0e12584",
    "SessionId": "36b8ed87-0a5e-4b80-aa81-926bbed564ec",
    "UserId": 339,
    "EventType": "cart_purchased",
    "Product": null,
    "QuantityChange": null,
    "ProductsInCart": null,
    "EventTimestamp": "2024/03/27 14:27:13",
    ...
}

いずれもデータとしては独立している(前後のイベントを意識しない)ため、そのデータを再送信することも可能です。

このように、いつ・誰が・どこへ・どのようなデータを更新操作したかというイベントデータを記録しておくことで、時系列でデータを検索するクエリを発行することが可能となります。

SELECT * FROM c
WHERE c.CartId = 'd5e3f139-fafe-4acd-91cf-0fbab0e12584'
ORDER BY c.EventTimestamp ASC
[
    {
        "id": "a6e8d56b-4ffb-4a01-a6c1-105955ff70fc",
        "CartId": "d5e3f139-fafe-4acd-91cf-0fbab0e12584",
        "SessionId": "36b8ed87-0a5e-4b80-aa81-926bbed564ec",
        "UserId": 339,
        "EventType": "cart_created",
        "Product": null,
        "QuantityChange": null,
        "ProductsInCart": null,
        "EventTimestamp": "2024/03/27 14:27:13",
        "_rid": "DkMRAIy4LC0BAAAAAAAAAA==",
        "_self": "dbs/DkMRAA==/colls/DkMRAIy4LC0=/docs/DkMRAIy4LC0BAAAAAAAAAA==/",
        "_etag": "\"0100a85d-0000-2300-0000-66042cc40000\"",
        "_attachments": "attachments/",
        "_ts": 1711549636
    },
    {
        "id": "107ad5b5-eb9d-4263-b7ed-cab34542f979",
        "CartId": "d5e3f139-fafe-4acd-91cf-0fbab0e12584",
        "SessionId": "36b8ed87-0a5e-4b80-aa81-926bbed564ec",
        "UserId": 339,
        "EventType": "product_added",
        "Product": "Product 1",
        "QuantityChange": 0,
        "ProductsInCart": [
            {
                "ProductName": "Product 1",
                "Quantity": 0
            }
        ],
        "EventTimestamp": "2024/03/27 14:27:13",
        "_rid": "DkMRAIy4LC0CAAAAAAAAAA==",
        "_self": "dbs/DkMRAA==/colls/DkMRAIy4LC0=/docs/DkMRAIy4LC0CAAAAAAAAAA==/",
        "_etag": "\"0100a95d-0000-2300-0000-66042cc40000\"",
        "_attachments": "attachments/",
        "_ts": 1711549636
    },
    {
        "id": "55a47e16-c5eb-455e-9a4d-59c612222f2d",
        "CartId": "d5e3f139-fafe-4acd-91cf-0fbab0e12584",
        "SessionId": "36b8ed87-0a5e-4b80-aa81-926bbed564ec",
        "UserId": 339,
        "EventType": "product_added",
        "Product": "Product 2",
        "QuantityChange": 3,
        "ProductsInCart": [
            {
                "ProductName": "Product 1",
                "Quantity": 0
            },
            {
                "ProductName": "Product 2",
                "Quantity": 3
            }
        ],
        "EventTimestamp": "2024/03/27 14:27:13",
        "_rid": "DkMRAIy4LC0DAAAAAAAAAA==",
        "_self": "dbs/DkMRAA==/colls/DkMRAIy4LC0=/docs/DkMRAIy4LC0DAAAAAAAAAA==/",
        "_etag": "\"0100aa5d-0000-2300-0000-66042cc40000\"",
        "_attachments": "attachments/",
        "_ts": 1711549636
    },
    {
        "id": "07cf958e-b08b-485b-a204-e7ab91b42ca0",
        "CartId": "d5e3f139-fafe-4acd-91cf-0fbab0e12584",
        "SessionId": "36b8ed87-0a5e-4b80-aa81-926bbed564ec",
        "UserId": 339,
        "EventType": "product_deleted",
        "Product": "Product 2",
        "QuantityChange": -1,
        "ProductsInCart": [
            {
                "ProductName": "Product 1",
                "Quantity": 0
            }
        ],
        "EventTimestamp": "2024/03/27 14:27:13",
        "_rid": "DkMRAIy4LC0EAAAAAAAAAA==",
        "_self": "dbs/DkMRAA==/colls/DkMRAIy4LC0=/docs/DkMRAIy4LC0EAAAAAAAAAA==/",
        "_etag": "\"0100ab5d-0000-2300-0000-66042cc40000\"",
        "_attachments": "attachments/",
        "_ts": 1711549636
    },
    {
        "id": "edd4eda9-fdd1-428a-8e49-84839cbd1f54",
        "CartId": "d5e3f139-fafe-4acd-91cf-0fbab0e12584",
        "SessionId": "36b8ed87-0a5e-4b80-aa81-926bbed564ec",
        "UserId": 339,
        "EventType": "cart_purchased",
        "Product": null,
        "QuantityChange": null,
        "ProductsInCart": null,
        "EventTimestamp": "2024/03/27 14:27:13",
        "_rid": "DkMRAIy4LC0FAAAAAAAAAA==",
        "_self": "dbs/DkMRAA==/colls/DkMRAIy4LC0=/docs/DkMRAIy4LC0FAAAAAAAAAA==/",
        "_etag": "\"0100ac5d-0000-2300-0000-66042cc40000\"",
        "_attachments": "attachments/",
        "_ts": 1711549636
    }
]

イベントソーシングパターンは、スケーラブルで回復力のあるアプリケーションを構築するために有効な実装方法です。
実践できるようにポイントを抑えておきたいです。


サービス一覧 www.alterbooth.com cloudpointer.tech www.alterbooth.com