KotlinでDynamoDBに接続 - その３

## 概要
DynamoDB Enhanced ClientでDynamoDBに接続する際によく使うコマンドをまとめました。

:::note warn
プロジェクト全体のソースコードはこちらに置いてあります。
https://github.com/ist-h-i/DynamoDbEnhancedClient
:::
## 事前準備
### DynamoDBのテーブル作成やアクセスキーの取得は前回の記事参照
:::note warn
テーブル作成方法参照
https://qiita.com/ist-h-i/items/64099bafd1f99b58059d
:::
## 実装
### スキーマ作成
```kotlin
private val userSchema: TableSchema<User> =
    TableSchema.builder(User::class.java).newItemSupplier { User() }
        .addAttribute(String::class.java) { a ->
            a.name("GroupName").getter(User::userId).setter { user, s -> user.userId = s }
                .tags(StaticAttributeTags.primaryPartitionKey())
        }.addAttribute(String::class.java) { a ->
            a.name("CognitoIdentityId").getter(User::password)
                .setter { user, s -> user.password = s }
                .tags(StaticAttributeTags.primarySortKey())
        }.build()
```
### クライアント作成
```kotlin
private var enhancedClient: DynamoDbEnhancedAsyncClient =
    DynamoDbEnhancedAsyncClient.builder().dynamoDbClient(
        DynamoDbAsyncClient.builder().region(
            Region.AP_NORTHEAST_1
        ).credentialsProvider(
            StaticCredentialsProvider.create(
                AwsBasicCredentials.create(BuildConfig.accessKeyId, BuildConfig.secretAccessKey)
            )
        ).build()
    ).build()
```
### テーブルインスタンス作成
```kotlin
private var userTable: DynamoDbAsyncTable<User> =
    enhancedClient.table("LoginAppUser", userSchema)
```
### GetItem
```kotlin
// 引数のオブジェクトを取得
fun getItem(user: User): User? {
    return userTable.getItem(user).getNow(null) // 即時で取得結果返却（取得できなかった場合はvalueIfAbsentの内容を返す）
}

// キーを指定して取得
fun getItemByKey(): User? {
    val user: CompletableFuture<User>? = userTable.getItem(
        Key.builder().partitionValue("abc") // パーティションキー指定
            .sortValue("123") // ソートキー指定
            .build()
    )
    return user?.join() // 結果取得まで待機
}

// リクエストで取得
fun getItemByRequest(): User? {
    val key = Key.builder().partitionValue("abc") // パーティションキー指定
        .sortValue("123") // ソートキー指定
        .build()

    val getItemRequest: GetItemEnhancedRequest =
        GetItemEnhancedRequest.builder().key(key).build()

    val user: CompletableFuture<User>? = userTable.getItem(getItemRequest)
    return user?.get(10L, TimeUnit.SECONDS) // 10秒でタイムアウト
}
```
### ScanItems
```kotlin
// 条件を指定せず取得し返却
fun scanItems(): MutableList<User> {
    val pagePublisher: PagePublisher<User> = userTable.scan()
    val subscriber = UserSubscriber()
    pagePublisher.subscribe(subscriber)

    val userList: MutableList<User> = ArrayList()
    pagePublisher.items().subscribe { user: User -> userList.add(user) }.exceptionally { null }
        .join()

    return userList
}

// 条件付きで取得
fun scanItemsByRequest(): MutableList<User> {
    val request = ScanEnhancedRequest.builder()
        .consistentRead(true) // 成功した以前のすべての書き込みオペレーションからの更新を反映した、最新データを含む応答を返却
        .attributesToProject("UserId", "Password", "Age") // 返却される属性を指定
        .filterExpression(
            Expression.builder()
                .expression("Age >= :min_value AND Age <= :max_value") // フィルター条件式
                .expressionValues(
                    mapOf(
                        Pair(":min_value", numberValue(6)), Pair(":max_value", numberValue(12))
                    )
                ).build()
        ).build()

    val pagePublisher: PagePublisher<User> = userTable.scan(request)
    val subscriber = UserSubscriber()
    pagePublisher.subscribe(subscriber)

    val userList: MutableList<User> = ArrayList()
    pagePublisher.items().subscribe { user: User -> userList.add(user) }.exceptionally { null }
        .join()

    return userList
}
```
### QueryItems
```kotlin
// キーを指定して取得
fun queryItemsByCondition(): MutableList<User> {
    val queryConditional = keyEqualTo { k: Key.Builder ->
        k.partitionValue("abc") // パーティションキー指定
            .sortValue("123") // ソートキー指定
    }

    val pagePublisher: PagePublisher<User> = userTable.query(queryConditional)
    val subscriber = UserSubscriber()
    pagePublisher.subscribe(subscriber)
    val userList: MutableList<User> = ArrayList()
    pagePublisher.items().subscribe { e: User -> userList.add(e) }.exceptionally { null }.join()
    return userList
}

// 条件付きで取得
fun queryItemsByRequest(): MutableList<User> {
    val keyEqual = keyEqualTo { k: Key.Builder ->
        k.partitionValue("abc") // パーティションキー指定
            .sortValue("123") // ソートキー指定
    }

    val tableQuery = QueryEnhancedRequest.builder().queryConditional(keyEqual)
        .scanIndexForward(true) // ソートきーでの昇順: true(デフォルト) / 降順: false
        .limit(5) // 取得件数を指定
        .build()

    val pagePublisher: PagePublisher<User> = userTable.query(tableQuery)
    val subscriber = UserSubscriber()
    pagePublisher.subscribe(subscriber)
    val userList: MutableList<User> = ArrayList()
    pagePublisher.items().subscribe { e: User -> userList.add(e) }.exceptionally { null }.join()
    return userList
}
```
### PutItem
```kotlin
// 引数のオブジェクトを格納
fun putItem(user: User) {
    userTable.putItem(user)
}

// リクエスト付きで格納
fun putItemByRequest(user: User) {
    val expression =
        Expression.builder().expression("attribute_not_exists(#id)") // idが登録済みでない場合にリクエスト実行
            .expressionNames(mapOf(Pair("#id", "UserId"))) // DynamoDBのkey名
            .build()

    val putItemEnhancedRequest: PutItemEnhancedRequest<User> =
        PutItemEnhancedRequest.builder(User::class.java).item(user)
            .conditionExpression(expression).build()

    userTable.putItem(putItemEnhancedRequest)
}
```
### UpdateItem
```kotlin
// 引数のオブジェクトで値を更新
fun updateItem(user: User) {
    userTable.updateItem(user) // NULL値の属性は項目から削除される
}

// 引数のオブジェクトのNULLでない値を更新
fun updateItemIgnoreNulls(user: User) {
    userTable.updateItem { b ->
        b.item(user).ignoreNulls(true) // NULL値でない属性を更新する
    }
}

// リクエスト付きで値を更新
fun updateItemByRequest(user: User) {
    val updateItemEnhancedRequest =
        UpdateItemEnhancedRequest.builder(User::class.java).item(user).ignoreNulls(true).build()

    userTable.updateItem(updateItemEnhancedRequest)
}
```
## まとめ
DynamoDB Enhanced Clientでよく使うコマンドをまとめてみました。
コマンドがシンプルであったり条件式が共通していたりと学習コストが低く、手を出しやすい印象でした。
次回は今回まとめたもの以外のコマンドも記事にしていきたいと思います。