🐙

Node.js × DynamoDBの強い味方「Dynamoose」の紹介

2021/11/26に公開

Dynamooseとは?

DynamooseとはMongooseにインスパイアされたDynamoDB用のモデリングツールになります。
ドキュメントにおいてKey Featuresとして以下が紹介されています。

- Type safety
- High level API
- Easy to use syntax
- Ability to transform data before saving or retrieving documents
- Strict data modeling (validation, required attributes, and more)
- Support for DynamoDB Transactions
- Powerful Conditional/Filtering Support
- Callback & Promise support

TypeScriptにも対応していますが、ドキュメント上ではまだベータという扱いになっています。

使い方

Dynamooseで重要な概念として、SchemaModelがあります。
SchemaはDynamoDBに保存する値の型や必須などの属性を指定します。typeに指定しているのはTypeScriptの型ではなくDynamoose上の型になります。

import * as dynamoose from "dynamoose";

const UserSchema = new dynamoose.Schema({
    id: {
        type: Number,
        hashKey: true,
    },
    age: {
        type: Number,
        default: 5,
    },
    name: {
        type: String,
        enum: ["Tom", "Tim"],
    },
    friends: {
        type: Array,
        required: true,
        schema: {
            type: String,
        },
    },
});

このような形でかなり直感的に書けると思います。これ以外にも挿入・取得前にデータ変換やバリデーションなど多くのオプションを指定することが出来ます。

ModelはDynamoDB上のテーブルを表します。以下のような形でテーブル名とスキーマを指定します。またgetなどの操作もModelから行うことが可能です。
TypeScriptの型を使うには別で宣言し、Modelにジェネリクスで渡してあげる必要があります。こうすることでTSの恩恵にあずかれます。

import * as dynamoose from "dynamoose";
import { Document } from "dynamoose/dist/Document";

interface IUser extends Document {
    id: number;
    age: number;
}

const UserSchema = new dynamoose.Schema({
    id: {
        type: Number,
        hashKey: true,
    },
    age: {
        type: Number,
        required: true,
        default: 5,
    },
});

// Userテーブル
const UserModel = dynamoose.model<IUser>("User", UserSchema);

// id=1のユーザーを取得する
try {
    const myUser = await UserModel.get(1);
    console.log(myUser);
} catch (error) {
    console.error(error);
}

最大の特徴として、1つのModelに対して、複数のSchemaを指定出来るところにあります。
DynamoDBではベストプラクティスとしてテーブル数を出来るだけ減らすことが推奨されています。よくsingle-table designとして紹介されています。
以下のように配列で複数のSchemaを渡すことで、single-table designに最適化されたモデリングをすることが出来ます。

// ここでは1テーブルにAdminユーザーと一般ユーザーを格納する想定
const UserModel = dynamoose.model<IAdminUser | IUser>("User", [
    AdminUserSchema,
    UserSchema,
]);

どのSchemaを使用するかは挿入・取得するデータに合わせてDynamooseが自動的に選択してくれます。

少し内部の実装を読んでみた際のメモ(正しいとは限らないです)

ちょうどこの部分でどのスキーマを選択するかを決定しています。
この例では1つのテーブルにAdminUserとUserを格納すると想定します。

Schemaと実際にデータ構造を比較して、以下のようなオブジェクトを作成しmatchCorrectnessの最低値が1番大きいスキーマが選択されるようです。
以下の例だと1番目(AdminUserSchemaとのmatchCorrectness)の最低値は0、2番目(UserSchemaとのmatchCorrectness)の最低値は1なので、2番目で比較を行ったSchemaが選択されることになります。
最低値同士を比較するので1つでもSchemaにマッチしない値(matchCorrectnessが0)があるとそのSchemaは選択されなくなります。

// 1つのオブジェクトは実際のデータとSchemaの近似性を表す
[
  // AdminUser用Schemaとの比較
  {
    "id": {
      "index": 0,
      "matchCorrectness": 1, // Schemaにマッチ
      "entryCorrectness": [
        1
      ]
    },
    "age": {
      "index": 0,
      "matchCorrectness": 0, // Schemaにマッチしない
      "entryCorrectness": [
        0
      ]
    }
  },

  // 一般User用Schemaとの比較
  {
    "id": {
      "index": 0,
      "matchCorrectness": 1, // Schemaにマッチ
      "entryCorrectness": [
        1
      ]
    },
    "age": {
      "index": 0,
      "matchCorrectness": 1, // Schemaにマッチ
      "entryCorrectness": [
        1
      ]
    }
  }
]

そのため、意図したSchemaをDynamooseが使用してくれない場合はSchemaと実際のデータが一致しているかを確認することでデバックが可能です

まとめ

この記事では使い方というより特徴的な部分に絞って紹介を行いました。
Node.jsからDynamoDBを操作する際は強い味方になると思うのでぜひ使ってみてください!

Github: https://github.com/dynamoose/dynamoose
Docs: https://dynamoosejs.com/getting_started/Introduction

Discussion