SCSKの畑です。6回目の投稿です。
今回も引き続きアプリケーションにおける排他制御の話題ですが、よりアプリケーション側の実装にフォーカスしています。
はじめに
第一回で整理していた排他制御の要件は以下の通りです。
- 主にデータベース/DWH 上のテーブルデータのメンテナンスを実施するアプリケーション
- 以下の要件に基づき、アプリケーション側で排他制御(ロック)を実装
- テーブルデータ編集時に、テーブル単位での排他制御(ロック)を行う
- 外部キーを持つテーブルのデータ編集時には、外部キーの参照先テーブルについても合わせて排他制御(ロック)を行う
- 対象テーブルのステータス(編集状態)は DynamoDB で管理する
- ステータスの遷移(更新)時に、DynamoDB、Lambda、AppSync を用いて排他制御(ロック)を行う
ここ2回のエントリでは主に4点目の内容について記載しましたが、今回は2点目の内容について記載します。
今回の案件事例では、2点目の内容を満たす設計・実装として、具体的には以下のような方式を採りました。
- あるテーブルをユーザAが編集中の場合、
- ユーザAにはテーブルデータの編集画面を表示する
- それ以外のユーザにはテーブルデータの参照画面を表示する(編集はできない)
この場合、DynamoDB で管理されているテーブルのステータスを元に、ユーザ毎にアプリケーション側で画面を制御したり、ルーティングを変更したりする必要があります。よって、このステータスをどのようにアプリケーション内で扱うかが重要なポイントとなったため、考慮・工夫した点を以下に記載していきたいと思います。
アーキテクチャ概要
こちらも載せておきます。
本投稿で言及するのはほぼアプリケーション(Nuxt.js)部分になりますが、ちょこちょこ AppSync の話も書きます。なお、本投稿では Nuxt.js(Vue)自体の説明は割愛します。ご了承ください。
- Amazon DynamoDB
- テーブルのステータス(編集状態)管理
- AWS Lambda
- 排他制御を考慮したテーブルのステータス更新ロジックの実装
- AWS AppSync(AWS Amplify)
- 上記 Lambda をアプリケーション上から実行するためのスキーマ定義
- アプリケーション(Nuxt.js)
- テーブルのステータスに応じた画面制御
テーブルのステータスをコンポーネント間で共有する
冒頭の繰り返しになりますが、テーブルのステータス(編集状態)をベースにアプリケーション側でルーティングや画面の制御を行う必要があるため、同情報はアプリケーション内の各コンポーネントから参照できることが望ましいです。このような仕組みをどのように実装するかはフレームワークに拠りますが、Nuxt3 の場合は useState を使用すると楽に実装できます。
実装にあたっては以下サイト様の内容がとても参考になりました。
以下実装例です。composable として実装しています。
import * as custom_queries from "@/src/graphql/customQueries"; import * as models from "@/src/API"; export type TableStatus = { status: models.TableStatus, editor: string, locked_by: string, } export const useTableStatus = () => { // stateの定義 const TableStatusDict:Ref<{[key: string]: TableStatus}> = useState('g_TableStatusDict', () => ({})) // stateの更新処理 const updateTableStatusDict = async () => { const client = useNuxtApp().$Amplify.GraphQL.client; const result=await client.graphql({ query: custom_queries.listTablesCustom, }) let master_table_status:{[key: string]: TableStatus} = {} for (let item of result.data.listTableInfos.items) { master_table_status[item.name] = { 'status': item.status, 'editor': item.editor, 'locked_by': item.locked_by, } } TableStatusDict.value = master_table_status; } return { TableStatusDict: readonly(TableStatusDict), updateTableStatusDict: updateTableStatusDict, } }
useState() で定義したオブジェクトは Nuxt3(Vue3) における Ref となるため、typescript 内でアクセスする際は .value を付けてアクセスする必要があります。また、export 時は readonly として読み取り専用とし、内容の更新は専用のメソッド「updateTableStatusDict」を通してのみ実行するようにしています。
更新処理自体は、前回のエントリで定義した DynamoDB のテーブルの内容を graphql 経由で取得してきた上で、dict 形式に変換してuseState() で定義したオブジェクトを更新しているだけですね。
テーブルのステータスをアプリケーション側にリアルタイム反映する
テーブルのステータスについてはなるべくリアルタイムでアプリケーション側に反映できるとユーザビリティの観点からは望ましいのですが、そのような仕組みを自前で用意するのは実装面でなかなかハードルが高いところです。そこで今回は、AppSync の subscription を使用したプッシュ通知によるリアルタイム更新を採用しました。この仕組みによりステータスの反映処理の大凡を任せることができて実装も楽になったので、正に一石二鳥でした。この仕組みを使いたいが故に今回 AppSync を使用したと言っても過言ではないかもしれません。
subscription については、平野さんが執筆された素晴らしいエントリがありますのでこちらも合わせてご参照ください。私自身も大いに参考にさせて頂きました。
以下、schema.graphql ファイルにおける実装例です。
type Subscription { onStatusChangeTableWithLock: ResultTableStatus @aws_subscribe(mutations: ["ChangeTableStatusWithLock"]) @aws_api_key @aws_iam }
今回 subscription を設定したいのは、前回のエントリで作成したステータス更新用の mutation (ChangeTableStatusWithLock) となるので、それを3行目の「mutations:」で指定したリスト内で定義すれば OK です。リスト内に複数の mutation を指定して、単一の subscription に対応させることも可能です。
以下実装例です。こちらは、メンテナンス対象のテーブル一覧を表示するためのメニュー用 component 内のメソッドとして定義しており、onMounted() のタイミングで実行するようにしています。
const subscribeTableList = async () => { varSubscriptionStatus.value = client .graphql({ query: subscriptions.onStatusChangeTableWithLock }) .subscribe({ next: async (data: any) => { await updateTableStatusDict() await updateTableMenu() }, error: (error: any) => console.warn(error) }); } onMounted(async () => { await updateTableMenu(); await subscribeTableList(); });
なお、 subscription の返り値は「mutations:」で指定した mutation と同一でないといけない点に注意してください。 mutation を複数指定する場合も同様です。
よって、今回の実装においてはアプリケーション側でこの subscription によるプッシュ通知を受け取った後に、1つ前のセクションで記載したようなテーブルステータス更新用のメソッドを実行する必要があります。この subscription の役割はあくまでいずれかのテーブルのステータスに更新があったことまでで、各テーブルのステータスは改めて取得する必要があるということですね。
前回及び今回の実装例における改善点があるとすると、この返り値である ResultTableStatus にテーブルステータス更新用のメソッドの返り値と同じような情報も含むようにすれば、AppSync API のコール数が1回減ってより効率的になったというところでしょうか。もちろんその場合は composable の仕様についても見直す必要がありますが。
ほか、メニュー画面にテーブルのステータスを表示している都合上画面も更新する必要があるため、そのための関数「updateTableMenu()」も合わせて実行しています。(同関数の内容については今回触れません)
aws-amplify の SSR (Nuxt3)対応
最後に、本筋とは少し異なる話題になりますが触れておこうと思います。
今回は Web アプリケーションフレームワークとして Nuxt3 を使用している関係上、レンダリングモードとして SSR (Server-Side Rendering) を使用しています。このため、厳密には実装次第ではありますが、aws-amplify モジュールの各関数がクライアントサイドだけでなく、サーバサイドでも実行され得ます。しかし、aws-amplify は原則 React や Vue などの SPA (Single-Page Application) で使用することが前提のため、Nuxt3 (Nuxt.js) において SSR 対応させるためには別途準備が必要となります。
具体的には、Nuxt3 の plugin を使用して、クライアントサイドとサーバサイドで使用する aws-amplify のインターフェース(関数)をそれぞれ分けて定義するような形式になります。ただ、サーバサイドで使用できる機能は限定されているため留意が必要です。詳細は以下 URL を参照ください。
なお、1つ目のセクションで示した実装例も、上記の通り plugin 内で初期化した graphql の client を使用しています。このため、コード内で aws-amplify モジュール を import していません。この composable は実装上クライアントサイド/サーバサイド両方で実行され得るため、ここまで記載したような対策が必要となります。
まとめ
いずれのセクションの内容もアプリケーションの設計・実装上は重要な要素だったと感じています。特に、aws-amplify の SSR 対応についてはアプリケーション開発当初に調べていた内容なのですが、調査の過程でアプリケーションフレームワーク自体の挙動や SPA との差異など、他の重要な知識についてもある程度知ることができたのが今振り返るととても良かったです。
第四回を書くかは現時点では未定ですが、書く場合は Nuxt.js における具体的なルーティングの実装例について幾つか記載してみようと思います。
本記事の内容がどなたかの役に立てば幸いです。