NodeDynamoDBORM というDynamoDB
でRails
のActiveRecord
のように使うことができるNode.js
ライブラリを作成しましたので紹介します。
よかったら、使ってください。またスターもください。
プルリクやご要望もお待ちしています。
DynamoDB
については後ろの方に紹介しているのでそちらもご覧ください。
イントロダクション
インストール
npm install node-dynamodb-orm --save
または、yarn
を使う場合は
yarn add node-dynamodb-orm
でインストールします。
特徴
Rails
のActiveRecord
の使用感に合わせた形で作成しました。ActiveRecord
と大体同じ感覚で使え、ActiveRecord
の機能をできるだけ再現できるように作りました。
なぜ作った?
公式のaws-sdkがすごく使いにくい!!
aws-sdk を使ってDynamoDB
を操作する開発に限界を感じ、使いやすい ORM も存在しなかったため作成しました。(共通の悩みを持っている人も多くいるようです(参考を参照))
機能の追加希望などの意見がありましたら教えてください。プルリクもお待ちしています。
参考
DynamoDB を使う
ローカル環境での開発環境の構築
プロジェクト(NodeDynamoDBORM)の中ではDynamoDB
の環境をDocker
を用いてローカル環境を構築し、Jestでテストを記述しています。
上記プロジェクトをダウンロード後、
docker-compose up
と実行することでDynamoDB
のローカルサーバーが起動します。
サーバーが起動した後 http://localhost:8000/shell/ にアクセスすることでDynamoDB
のJavascript Console
が開くので、こちらでいろいろと試すことができます。
参考
AWS コンソールから DynamoDB を使う
- AWS コンソールから DynamoDB を選択することで各種設定を行うことができます。
- 初めてならば、とりあえずテーブルを作成していくことで使用できます。
- 外部から DynamoDB にアクセスするためにはIAMにて DynamoDB へのアクセス権限を付与し、付与したアカウントの
accessKey
とsecretAccessKey
を取得します。
上記のように DynamoDB にアクセスすることができるアカウントのaccessKeyId
とsecretAccessKey
を用いることで、AWS コンソール上に作った DynamoDB の操作ができるようになります。
また AWS コンソールからだけでなくaws-cli
を用いてcli
からの操作も可能です。その場合は上記IAMにて取得した、accessKeyId
、secretAccessKey
を用いることで DynamoDB にアクセスすることができます。
Node.js での使い方
import/require
使用するためには以下のように読み込みます。
const { DynamoDBORM } = require("node-dynamodb-orm");
または TypeScript
など import
が使用できる場面では
import { DynamoDBORM } from 'node-dynamodb-orm';
これで宣言できます。
接続情報の設定
読み込んだら、DynamoDB
に接続するための接続情報を設定します。
process.env
内に以下に指定した値を設定されていれば自動的に読み込んで接続することもできます。そのため、dotenvを用いて.env
ファイルに記載した場合その設定が接続に反映されます。
.env への記述の仕方
接続情報の例を以下に示します。
region=DynamoDB's AWS Region
endpoint=endpoint (ローカルなら http://localhost:8000 それ以外はなくてもOK)
accessKeyId=xxxxx
secretAccessKey=xxxxxxxxxx
region
は必須です。
ローカルの DynamoDB に接続したい場合はendpoint
を指定して下さい。
accessKeyId=xxxxx
secretAccessKey=xxxxxxxxxx
は、上記 AWS コンソールでの設定にて、DynamoDB へのアクセスが可能な権限を持ったaccessKeyId
とsecretAccessKey
をそれぞれ使用することで使用することができます。
接続情報を直接指定する場合
ソースコードの中で直接指定することもできます。その場合は以下のように指定します。
DynamoDBORM.updateConfig({ region: 'ap-northeast-1', endpoint: 'http://localhost:8000' });
この場合、ローカルの DynamoDB へのアクセスとなります。
accessKeyId
とsecretAccessKey
を用いた場合は以下のようになります。
DynamoDBORM.updateConfig({ region: 'ap-northeast-1', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey' });
Node.js から呼び出す
今回、以下のようなusers
テーブルがすでに作成されているとします。
項目 | 値 |
---|---|
テーブル名 | users |
プライマリキー(ハッシュキー) | user_id |
プライマリキー(ハッシュキー)のデータ種別 | 文字列 |
また users
の持ち物として items
テーブルが以下のように定義され、すでに作成されているとします。 また items
テーブルは users
テーブルと依存関係にあります。
項目 | 値 |
---|---|
テーブル名 | items |
プライマリキー(ハッシュキー) | user_id |
プライマリキー(ハッシュキー)のデータ種別 | 文字列 |
プライマリキー(レンジキー) | mst_item_id |
プライマリキー(レンジキー)のデータ種別 | 文字列 |
これらのテーブルを用いた操作の例を以下に紹介していきます。
- ハッシュキー = パーティションキー
- レンジキー = ソートキー
ともいいます。
New
まずはテーブル名を引数にインスタンスを作成します。
const usersTable = new DynamoDBORM('users');
CRUD
Create
テーブルにデータを追加
const user = await usersTable.create({ user_id: 'user_id', name: 'sample' });
テーブル内のデータは以下のようになります。
user_id | name |
---|---|
user_id | sample |
Update
テーブル内のデータを更新
const user = await usersTable.update({ user_id: 'user_id' }, { email: 'xxxx', name: 'sample2' });
テーブル内のデータは以下のようになります。
user_id | name | |
---|---|---|
user_id | sample2 | xxxx |
※ DynamoDB ではプライマリキーのデータの更新はできないので注意してください
Read
テーブル内からデータを取得
const user = await usersTable.findBy({ user_id: 'user_id' });
//user => {user_id: "user_id", name: "sample2", email: "xxxx"}
※ この時、プライマリキーを指定しなければなりません。指定しなかった場合はエラーになります
Delete
テーブル内からデータを削除
const isDeleteSuccess = await usersTable.delete({ user_id: 'user_id' });
テーブル内のデータは以下のようになります。
user_id | name |
---|
一括操作系
ここでは items
テーブルへの操作例を紹介します。
const itemsTable = new DynamoDBORM('items');
データの一括追加
テーブルに複数件のデータを追加するには以下のようになります。
const items = await itemsTable.import([
{ user_id: 'user_id1', mst_item_id: 1, amount: 1 },
{ user_id: 'user_id1', mst_item_id: 2, amount: 2 },
{ user_id: 'user_id2', mst_item_id: 2, amount: 2 },
]);
items
テーブル内のデータは以下のようになります。
user_id | mst_item_id | amount |
---|---|---|
user_id1 | 1 | 1 |
user_id1 | 2 | 2 |
user_id2 | 2 | 2 |
複数のデータを取得
テーブル内から複数のデータを取得
const items = await itemsTable.findByAll({ user_id: 'user_id1' });
//=> [{ user_id: 'user_id1', mst_item_id: 1, amount: 1 },{ user_id: 'user_id1', mst_item_id: 2, amount: 2 }]
または where
を使って同様のこともできます。
const items = await itemsTable.where({ user_id: 'user_id1' }).load();
//=> [{ user_id: 'user_id1', mst_item_id: 1, amount: 1 },{ user_id: 'user_id1', mst_item_id: 2, amount: 2 }]
load()
を実行しないとデータを取得できないので、注意です。
テーブル内のデータ全件を取得したい
テーブル内に含まれているデータを全て取得することもできます。
const items = await itemsTable.all();
//=> [{ user_id: 'user_id1', mst_item_id: 1, amount: 1 },{ user_id: 'user_id1', mst_item_id: 2, amount: 2 },{ user_id: 'user_id', mst_item_id: 2, amount: 2 }]
ページングしたい
DynamoDB
では Query
(findByAll
, where
で使用) で取得できるデータは 1MB
までです。
途中までしか取得できなかったデータを取得するためには取得できなかった先の情報にアクセスできる必要があります。そのような場合はoffset
を指定することでデータを取得することができます。
const items = await itemsTable.offset({ user_id: 'user_id1', mst_item_id: 2 }).load();
//=> [{ user_id: 'user_id', mst_item_id: 2, amount: 2 }]
DynamoDB
の仕様上 offset
として指定できるのは プライマリキー
の組み合わせを指定します。
参考
一括データ削除
テーブル内から複数のデータを削除
const items = await itemsTable.deleteAll([{ user_id: 'user_id1', mst_item_id: 1 }, { user_id: 'user_id1', mst_item_id: 2 }]);
items
テーブル内のデータは以下のようになります。
user_id | mst_item_id | amount |
---|---|---|
user_id2 | 2 | 2 |
あれ?なんか一部 ActiveRecrod と違くない?
扱っている相手が DynamoDB なので、そこは少し仕様が異なります
トランザクション
更新系処理を一括に行います。
const itemsTable = new DynamoDBORM('items');
const items = await itemsTable.import([
{ user_id: 'user_id1', mst_item_id: 1, amount: 1 },
{ user_id: 'user_id2', mst_item_id: 2, amount: 2 },
]);
await itemsTable.transaction(async () => {
await itemsTable.create({ user_id: 'user_id3', mst_item_id: 3, amount: 3 });
await itemsTable.update({ user_id: 'user_id2', mst_item_id: 2}, { amount: 3 });
await itemsTable.delete({ user_id: 'user_id1', mst_item_id: 1 });
});
上のような例を実行すると、items
テーブルの中は
user_id | mst_item_id | amount |
---|---|---|
user_id2 | 2 | 3 |
user_id3 | 3 | 3 |
という結果になります。
また、インスタンスを作らず実行することもでき、その場合、上記の内容は
const itemsTable = new DynamoDBORM('items');
const items = await itemsTable.import([
{ user_id: 'user_id1', mst_item_id: 1, amount: 1 },
{ user_id: 'user_id2', mst_item_id: 2, amount: 2 },
]);
await DynamoDBORM.transaction(async () => {
await itemsTable.create({ user_id: 'user_id3', mst_item_id: 3, amount: 3 });
await itemsTable.update({ user_id: 'user_id2', mst_item_id: 2}, { amount: 3 });
await itemsTable.delete({ user_id: 'user_id1', mst_item_id: 1 });
});
とすることもできます。
transaction
メソッドの中では複数のテーブルに更新を行う場合であっても、一括に実行してくれます。
※注意点として、MySQL等のトランザクションとは異なり、同じアイテムを同時に更新することはできません
Error: ValidationException Transaction request cannot include multiple operations on one item
参考
もう少し使っている感じをイメージしたい
Jest でテストを記述してあります。こちらを参照した方が使い方のイメージがつかみやすいかもしれません。そのほかの機能についても記述しています。
DynamoDB の基礎知識
そもそもとして、DynamoDB
を扱う上での基礎知識や特徴、仕様について以下に紹介します。
DynamoDB の魅力
- 安価(無料)で利用できる NoSQL データベース!!
- オートスケールもしてくれる → 高負荷にも強い
- 大規模データの保存/ハッシュキーによるデータの取得が高速
- カラムを追加する必要がない
DynamoDB デメリット
- 検索するときはプライマリキー(ハッシュキー)を指定しなければならない
- 複雑な操作はできない(ソートとか JOIN とか GROUPING とか)
- バリデーションが弱い(例: プライマリキーが重複したものを追加しようとすると、エラーが起こらず、そのデータが「更新」される)
DynamoDB の特性・仕様の解説
DynamoDB
は根本的には KVS の NoSQL データベースです。そのため、RDB と同じ考え方操作することができません。また、RDB の考え方でデータ構造を作ると苦労するので気をつけてください。
1. 複合プライマリキーとして設定できるキーは 2 種類まで
複合プライマリキー(ハッシュキーとレンジキー)として設定できるキーは 2 種類までであるので、データベースの正規化を行う場合は制約がかかります。
上記のitems
テーブルがその例で、上記以上のリレーションを構築することはDynamoDB
ではできません。そのため、データ構造はよく考えて構築する必要があります。
2. AUTO INCREMENT なんてものは存在しない
プライマリキーとして指定した Key は重複してはならない値になります。MySQL
などには自動で重複しない値を設定してくれるAUTO INCREMENT
属性がありますが、DynamoDB
にはそのようなものはありません。そのため、[uuid(https://github.com/kelektiv/node-uuid) などを使い、以下のように自力で重複しない値を生成する必要があります。
const uuid = require('uuid/v4');
uuid();
3. 検索する時はプライマリキー(ハッシュキー)の指定が必須
例えばMySQL
では上記のitems
テーブルのようなデータの場合
SELECT * FROM items WHERE mst_item_id = 2;
のようにレンジキーのみを指定したり
SELECT * FROM items WHERE amount = 2;
他のカラムのみを検索条件に指定して SQL で検索することができます。
しかし、DynamoDB ではハッシュキーである user_id
が等価であることを指定した上で他の条件を入力しなければエラーになってしまいます。
以上のようにDynamoDB
は RDB と性質が異なるデータベースであるため。用途に合わせるか使い方を工夫して使用する必要があります
参考
今後実装予定の機能について
四則演算、前方一致
実装予定の使用例
const itemsTable = new DynamoDBORM('items');
const items = await itemsTable.where('amount < 2').load();
find_each, find_in_batches
実装予定の使用例
const itemsTable = new DynamoDBORM('items');
itemsTable.findEach((item) => {
// 何か処理を書く
});
いずれも現在、ブランチを作成して機能を作成しています。作成までしばしお待ちを。
最後に
ぜひ使ってみてください。
また、バグとかありましたらご報告いただけると幸いです。(プルリクだとなお嬉しい)