DynamoDB Toolbox v 1.0 beta がでたので触ってみた
はじめに
こんにちは、back checkで SWE をしているぐっきーです。
最近 back check ではプロダクトの一角で DynamoDB を使い始めました。
DynamoDB 周りの使用技術としては主に Marshaling 目的で DynamoDBDocumentClient を、 Entity の定義と API パラメータの作成に DynamoDB Toolbox を使っています。
DynamoDBDocumentClient: DynamoDB とやりとりするデータは DynamoDB JSON という、primitive 型を含めた独自の JSON 形式で行われるため、これと可読性の高い一般的なデータ構造の JSON フォーマットと変換する役割。
DynamoDB Toolbox: Entity を DynamoDB の(主にシングルテーブルを想定)テーブルにマッピングしたり、Entity のスキーマに沿った API パラメータを作成してくれる役割。ただし型のサポートがいまいち行き届いていない部分があり、便利だけど改善してほしい部分もあるという温度感のツール。
そんなタイムリーなタイミングで DynamoDB Toolbox の v1.0.0 beta がリリースされたという記事を発見したので今回はどのような変更があったのか触ってみます。
結論
このブログにも書いてあるが、まだtransaction系だったりscanなどのAPIがサポートされていないこともあり、プロダクトで使うにはこれらのAPIが実装されてからアップデートを行った方がよさそうに思った。
個人的にはMapなどの内部まで型が適用できることによるポリモーフィズムの型推論が可能になったことなどポジティブな影響は大きく、latestとしてリリースされることが楽しみです。
今後の動向を要チェックといったところですね。
主な変更内容
- 型の対応範囲が広がった。
- betaで提供されるAPIはツリーシェイキングが効果的に行われるように書き直されているため、それぞれV2のAPIを利用することで軽量になる。
- TableとEntityクラスのIFが大きく変更されたことにより、より型安全なコマンドやEntityを作成することができるようになった。
- 既存のDynamoDBのデータ構造を保ちながらbeta版のAPIを利用することができる。
- 0系から beta へアップデートの際に migration は必要
具体的にはこちら The DynamoDB-Toolbox v1 beta is here 🙌 All you need to know! の記事にて詳細な変更内容が掲載されているので割愛します。
触ってみた
先にコードをみたいという方はこちら
今回大きな変更のあった箇所を中心に触ってみました。
TableV2 クラス
まずは TableV2 クラス。(旧Tableクラス)
主な変更点としては、partitionKey, sortKey が今まで文字列を直接指定するのみだったことに対して、v1.0.0 beta の変更では partitionKey, sortKey にプリミティブ型を指定することができるようになりました。
// v1.0.0 beta
const TableA = new TableV2({
documentClient: DynamoDBDocumentClient.from(new DynamoDBClient({})),
name: 'tableA',
partitionKey: {
name: 'pk',
type: 'string',
},
sortKey: {
name: 'sk',
type: 'string',
},
})
// v0.8.5
const TableA = new Table({
indexes: {
GSI1: { partitionKey: 'gsi1pk', sortKey: 'gsi1sk' },
},
name: 'tableA',
partitionKey: 'pk',
sortKey: 'sk',
})
懸念点として今まで Global Secondury Index の設定などをTable の indexes オプションで行っていたのですが、indexes がなくなったことでどのように GSI を指定するのかわからなくなりました。(もしかしたらそもそもの使い方が間違っていた可能性はありますが)
EntityV2 クラス
次に EntityV2 クラス。(旧Entityクラス)
大きな変更としては今まで attributes として定義していたスキーマが、schema メソッドに置き換わりました。
export const TableAEntity = new EntityV2({
name: "TableAEntity",
schema: schema({
pk: string().key(),
sk: string().key(),
}),
table: TableA,
})
また、timestamps オプションを設定することで作成、変更の日時を任意の名前で管理できるようになりました。
export const Entity = new EntityV2({
...schema,
timestamps: {
created: {
name: 'creationDate',
savedAs: '__createdAt__',
},
modified: {
name: 'lastModificationDate',
savedAs: '__lastMod__',
},
}
})
Attributes の型
全部は紹介しませんが、Attributes の型の設定の仕方に大きく変更がありました。
number 型
schema: schema({
// number
age: number(),
})
string 型
schema: schema({
// string
email: string(),
})
PrimaryKey の指定 (string)
schema: schema({
// PrimaryKey
pk: string().key(),
})
enum
schema: schema({
// type gender = 'male' as const | 'female' as const | 'other' as const
gender: string().enum('male', 'female', 'other'),
})
list
schema: schema({
/*
* skillsByList: string[]
*/
skillsByList: list(string()),
})
map
schema: schema({
/*
* skillsByMap: {
* karate: '白帯' | '茶帯' | '黒帯',
* kendo: '初段' | '二段' | '三段',
* }
*/
skillsByMap: map({
karate: string().enum('白帯', '茶帯', '黒帯'),
kendo: string().enum('初段', '二段', '三段'),
}),
})
record<any, any>
schema: schema({
// Record<string, string>
skillsByRecord: record(string(), string()),
})
set
schema: schema({
// Set<string>
skillsBySet: set(string()),
})
anyOf
schema: schema({
/*
* job: {
* type: 'engineer',
* } | {
* licenseStartDate: string,
* type: 'doctor',
* }
*/
job: anyOf([
map({
type: string().const('engineer'),
}),
map({
licenseStartDate: string().required(),
type: string().const('doctor'),
})
]),
})
any
schema: schema({
// any
metadata: any(),
})
Commands
各コマンドにも専用のクラスが用意されました。
尚、前述しましたが現時点でサポートされているのは Put
, Get
, Delete
のみでありその他のコマンドについては今後実装していく予定とのことです。
PutItemCommand
const dummyData = {
age: 30,
email: 'example@example.com',
gender: 'male' as const,
job: {
type: 'engineer' as const,
},
name: 'John Doe',
pk: 'user_123',
sk: 'profile',
skillsByList: ['JavaScript', 'Python', 'SQL'],
skillsByMap: {
karate: '黒帯' as const,
kendo: '初段' as const,
},
skillsByRecord: {
framework: 'React',
language: 'English',
},
skillsBySet: new Set<string>(['Guitar', 'Singing']),
};
export const putCommand = async (): Promise<void> => {
await TableAEntity.build(PutItemCommand).item(dummyData).options({
condition: {
attr: 'pk',
exists: false,
}
}).send();
}
GetItemCommand
export const getCommand = async (primaryKey: PrimaryKey<typeof TableA>): Promise<string> => {
const { pk, sk } = primaryKey
const { Item } = await TableAEntity.build(GetItemCommand).key({ pk, sk }).send();
if (Item === undefined) {
throw new Error('Item is not found')
}
if (Item.job.type === 'doctor') {
return `${Item.name} is a doctor. License start date is ${Item.job.licenseStartDate}`
}
return `${Item.name} is a ${Item.job.type}`
}
DeleteItemCommand
export const deleteCommand = async (primaryKey: PrimaryKey<typeof TableA>): Promise<void> => {
const { pk, sk } = primaryKey
await TableAEntity.build(DeleteItemCommand).key({ pk, sk }).send();
}
Thanks
DynamoDB のサンプルコードのベースをお借りした mukaihajime さん、ありがとうございました!
Discussion