KotlinでDynamoDBに接続 - その２

## 概要
DynamoDB Enhanced ClientでDynamoDBに接続する簡易ログインアプリを作成しました。

:::note warn
プロジェクト全体のソースコードはこちらに置いてあります。
https://github.com/ist-h-i/DynamoDbEnhancedClient
:::
## AWSの設定
### DynamoDB
テーブルを作成

:::note warn
作成方法参照
https://qiita.com/ist-h-i/items/c77b2858d87af4522e0a
:::
アクセスキーID、シークレットアクセスキーを取得
AWSコンソール > IAM
![IAM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/a35ac0c6-39c8-c099-59d1-5b19fc6973e4.png)

ユーザーを作成
![IAM User.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/6ecf2bee-18c0-2785-77ab-1db11b33b03d.png)
![Create User.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/6d6a9ff5-1c83-9c71-607e-704663fa1a65.png)

セキュリティ認証情報からアクセスキーを作成
![Create Key.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/3cea205e-4476-63df-0a58-2d6076cce686.png)
![External App.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/a6641bdf-e75c-5de6-17ea-d45501de7da0.png)

アクセスキーID、シークレットアクセスキーを控えるか.csvファイルをダウンロード
![Key Info.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/f74d443c-9b4d-fa4f-c597-1077d9bb16fd.png)

:::note alert
シークレットキーはキー作成時しか参照できないため注意
:::

## アプリ実装
### MainActivity.kt
```kotlin
package com.example.enhancedclient

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.enhancedclient.api.DynamoApi
import com.example.enhancedclient.databinding.ActivityMainBinding
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // レイアウト紐付け
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // ログインボタン押下時の設定
        binding.loginButton.setOnClickListener {
            val userId: String = binding.userId.text.toString()
            val password: String = binding.password.text.toString()

            // 未入力欄がある場合
            if (userId.isBlank() || password.isBlank()) {
                showLongToast(R.string.inputUserInfo)
                return@setOnClickListener
            }

            // 外部通信のためスレッド生成
            val queryDynamoDb = Thread {
                try {
                    val dynamoApi = DynamoApi()
                    // Query結果
                    val userRecord = dynamoApi.getDoesExistUser(userId, password)
                    if (userRecord) {
                        // User情報が登録済みの場合
                        showLongToast(R.string.loginSuccess)
                        // ログイン後後続処理はこの下に記載
                    } else {
                        // User情報がない場合
                        showLongToast(R.string.loginFailed)
                    }
                } catch (error: DynamoDbException) {
                    showLongToast(R.string.unknownError)
                }
            }
            queryDynamoDb.start()
            try {
                queryDynamoDb.join()
            } catch (error: InterruptedException) {
                Toast.makeText(this, R.string.unknownError, Toast.LENGTH_LONG).show()
            }
        }
    }

    private fun showLongToast(messageInt: Int) {
        // 別スレッドからUIを操作
        val handler = Handler(Looper.getMainLooper())
        handler.post {
            Toast.makeText(this, messageInt, Toast.LENGTH_LONG).show()
        }
    }
}
```

### DynamoApi
DynamoDBと接続してデータを読み込むクラス
```kotlin
package com.example.enhancedclient.api

import android.app.Activity
import com.example.enhancedclient.BuildConfig
import com.example.enhancedclient.model.User
import com.example.enhancedclient.model.UserSubscriber
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient
import software.amazon.awssdk.enhanced.dynamodb.Key
import software.amazon.awssdk.enhanced.dynamodb.TableSchema
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags
import software.amazon.awssdk.enhanced.dynamodb.model.PagePublisher
import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional
import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient

class DynamoApi : Activity() {
    // スキーマ作成
    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()

    // クライアント作成
    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()

    //テーブルインスタンス作成
    private var userTable: DynamoDbAsyncTable<User> =
        enhancedClient.table("LoginAppUser", userSchema)

    // 条件に合うユーザーが登録されているか確認
    fun getDoesExistUser(userId: String, pass: String): Boolean {
        val keyEqual = QueryConditional.keyEqualTo { b: Key.Builder ->
            b.partitionValue(userId).sortValue(pass)
        }
        val tableQuery = QueryEnhancedRequest.builder().queryConditional(keyEqual).limit(1).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.isNotEmpty()
    }
}
```

### User
Modelクラス
```kotlin
package com.example.enhancedclient.model

class User {
    var userId: String? = null
    var password: String? = null

    constructor(
        userId: String?,
        password: String?,
    ) {
        this.userId = userId
        this.password = password
    }

    constructor()
}
```

### UserSubscriber
読み込んだ各ページからアイテムを収集するクラス
```kotlin
package com.example.enhancedclient.model

import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription
import software.amazon.awssdk.enhanced.dynamodb.model.Page
import java.util.Objects
import java.util.concurrent.CountDownLatch
import java.util.stream.Collectors

class UserSubscriber : Subscriber<Page<User>> {
    // 読み込み完了まで待機するためのLatch
    private val latch = CountDownLatch(1)
    private val itemsFromAllPages: List<User> = ArrayList()
    private var subscription: Subscription? = null

    override fun onSubscribe(sub: Subscription?) {
        subscription = sub
        subscription!!.request(1L)
        try {
            // 待機開始
            latch.await()
        } catch (e: InterruptedException) {
            throw RuntimeException(e)
        }
    }

    override fun onNext(userPage: Page<User>) {
        userPage.items()?.let {
            itemsFromAllPages.stream().filter { obj: User? -> Objects.nonNull(obj) }
                .collect(Collectors.toList<Any>()).addAll(it)
        }
        subscription!!.request(1L)
    }

    override fun onError(throwable: Throwable?) {}

    override fun onComplete() {
        // 待機完了
        latch.countDown()
    }

    fun getSubscribedItems(): List<User> {
        return itemsFromAllPages
    }
}
```

### gradle.properties
アクセスキーID、シークレットアクセスキーを追加
```
accessKeyId=xxxxxxxxxxxxxxxxxxxx
secretAccessKey=xxxxxx/xxxxxxxxxxxxxxxxxxxxxxxxxxxx+xxxx
```

### build.gradle.kts
defaultConfigに設定値を追加
```
android {
    defaultConfig {
        buildConfigField("String", "accessKeyId", "\"${project.properties["accessKeyId"]}\"")
        buildConfigField("String", "secretAccessKey", "\"${project.properties["secretAccessKey"]}\"")
    }
}
```

### drawableのリソース等は以下参照
https://github.com/ist-h-i/DynamoDbEnhancedClient
## まとめ
DynamoDB Enhanced Clientを使ってDynamoDBにアクセスしました。標準のDynamoDB Clientと比較しテーブル作成やデータ操作を簡潔かつ直感的にコーディングできるため使いやすい印象でした。
また既存のモデルクラスを変更することなくスキーマ作成できることも大きなメリットであると感じました。
標準のDynamoDB Clientと比較してみてください。

:::note warn
KotlinでDynamoDBに接続 - その１
https://qiita.com/ist-h-i/items/c77b2858d87af4522e0a
:::