Node.js製DynamoDB ORM「ElectroDB」がすごい
by Shigeru Hyodo, Developer at RevisNote
RevisNoteではDBにAWSのNoSQL DB「DynamoDB」を使っているのですが、そのDynamoDBのNode.js製ORM「ElectroDB」がかなり優れており、システム開発の効率をぐんと引き上げています。今回はそんなElectroDBの紹介をします。
ElectroDBとは
単一テーブルデザイン(Single Table Design)で効果的にDynamoDBのテーブル設計と実装を行うことができる随一のNode.js製ORMです。
https://electrodb.dev/en/core-concepts/introduction/
GitHubでもStar数がそろそろ1000に届く勢いで、AWSの強力なサーバレス開発ツールSSTでも、ElectroDBを使ったサンプルが提供されています。
単一テーブルデザインってなに?
DynamoDBのテーブル設計のうち、代表的な設計パターンの1つです。文字通り単一のテーブルで、全ての情報を管理するデザインで、DynamoDBによるアプリケーション構築の場合、特別な理由がない限りは単一テーブルデザインにすることで、DynamoDBの強みであるスケーラビリティを最大限に活かすことができます。
旧来は「単一テーブル設計にすべき」くらいの強さでAWS公式が推奨していたのですが、最近は「必ずしも単一テーブルデザインでなくても良い」「しかし単一テーブルデザインは変わらずDynamoDBにおける強力な設計パターンの一つである」というスタンスに変わっているようです。
実際のところ、わたしもRevisNote以外でも単一テーブルデザインでDynamoDBを使うことが多いです。個人開発でも仕事でもこれは同じで、特に「サーバーレス構成で複雑なシステムを作りたい」場面でかつDynamoDBを使いたい状況なら、ほぼ間違いなく単一テーブルデザインを採用しています。
詳しくは下記の記事が単一テーブルデザインについて詳しく紹介しているので、興味のある方は読んでみてください。
Amazon DynamoDB におけるシングルテーブル vs マルチテーブル設計 | Amazon Web Services ブログ
ElectroDBの強み
単一テーブルデザインをライブラリの補助なく実装する場合、DynamoDBを操作するネイティブなAWS SDKのAPIを使う際に、操作を行うアイテムに関して、都度自前で用意した型定義に沿ったデータであることをなんとかして保証することで、堅安全性を確保します。これには、品質保証のためにそれなりの工数がかかりがちです。
一方でElectroDBを使えば、DynamoDBの単一テーブル上に保存する様々な種類のデータを「エンティティ」として、適切な型とインデックス定義、それから各種操作を行うためのメソッドを含むオブジェクトとして、簡単に定義していくことができます。
ElectroDB では、Entityエンティティは単一のビジネス オブジェクトを表します。たとえば、単純なタスク追跡アプリケーションでは、1 つのエンティティが従業員や従業員に割り当てられたタスクを表すことができます。
ElectroDB は、単一テーブル設計を念頭に置いて作成されました。その結果、ElectroDB によって作成されたエンティティは、ElectroDB によって作成されたエンティティから自動的に分離されます。つまり、単一の Dynamodb テーブルに、無制限の数の ElectroDB エンティティを含めることができることになります。
論より証拠ということで、まずはElectroDBによるDynamoDBのエンティティ定義を見てみましょう。
例えばこれはRevisNoteで実装中のお気に入り機能を司どるエンティティです(記事用に多少変えています)。
import { Entity, EntityItem } from "electrodb";
import { Dynamo } from "../dynamo";
export const FavEntity = new Entity(
{
model: {
version: "1",
entity: "Fav",
service: "revis",
},
attributes: {
/** レポートID */
reportId: {
type: "string",
required: true,
readOnly: true,
},
/** お気に入りした日時 */
favAt: {
type: "number",
required: true,
readOnly: true,
},
/** ユーザID */
userId: {
type: "string",
required: true,
readOnly: true,
},
/** 組織ID */
orgId: {
type: "string",
required: true,
readOnly: true,
},
},
indexes: {
// インデックスその1。特定のレポートについて、複数件のお気に入りを日時順で取得することに特化
byReportId: {
pk: {
field: "pk",
composite: ["reportId"],
},
sk: {
field: "sk",
composite: ["favAt", "orgId", "userId"],
},
},
// インデックスその2。特定のユーザがお気に入りしたレポートを日時順で取得することに特化
byOrgUser: {
index: "gsi1",
pk: {
field: "gsi1pk",
composite: ["orgId", "userId"],
},
sk: {
field: "gsi1sk",
composite: ["favAt", "reportId"],
},
},
},
},
Dynamo.Configuration
);
export type FavEntityType = EntityItem<typeof FavEntity>;
このように単一テーブル上に記録する予定のアイテムについて、そのアイテムが持つはずの属性に関してデータ型や必須項目、読み取り専用かどうかなどをエンティティとして定義しておくことで、ElectroDBはその型定義に従ってDynamoDBのアイテムを操作してくれます。
例えば、ここで定義したFavエンティティを作成する操作は、次のように簡単に行うことができます。
await FavEntity.create({
userId,
orgId,
favAt,
reportId,
}).go();
特定のレポートに紐づくお気に入りを一括取得するなら、こうです。
const { data: favs } = await FavEntity.query
.byReportId({
reportId,
})
.go();
さらに、DynamoDBには一度のクエリで走査できるアイテムの合計サイズが1MBまでという制限があるのですが、その制限を無視して全件取得する場合も、次のようにオプション指定で簡単に操作できます。
const { data: favs } = await FavEntity.query
.byReportId({
reportId,
})
.go({pages: "all"});
このようにElectroDBを用いれば、DynamoDBにおける代表的な設計パターン「単一テーブル設計(Signle Table Design)を型安全かつ迅速に実装できます。今回は紹介しませんが、少し複雑なデータ構造を作るとなるとすぐに必要になる複数エンティティ間のトランザクション操作なども、簡単に実装できるので、AWSをサーバーレス構成かつNode.jsメインで利用される方は、ぜひElectroDBを使ってみてください。
紹介: RevisNoteについて
RevisNoteは、スマホでもPCでも使える業務日報SaaSです。社内SNSとしての側面も備えており、その時思ったことを分報として記録する機能やチーム機能、コメント機能などを備えています。人日(何人が何日間使った)単位のシンプルな料金体系となっており、煩わしい申し込み手続きなしで、サインアップしてすぐに使えるのが特徴です。
無料プランもあるので、興味のある企業の方はぜひお試しください。