タケハタのブログ

プログラマの生き方、働き方、技術について雑多に書いていくブログです。

KotlessとDynamoDBで自分のツイートを収集するKotlinサーバーレスアプリケーションを作る

f:id:take7010:20211224093927p:plain 本記事は、Kotlin Advent Calendar 2021 24日目の記事になります。

以前書いたKotlessに関する記事で作ろうとしているアプリケーションがあると言っていたのですが、その実装でやろうとしていることを使って、ちょっとしたアプリケーションを作って紹介したいと思います。

Kotlessとは?

JetBrains社純正のKotlinのサーバーレスフレームワークです。
簡単に言うと、Kotlinの実装とGradleタスクでAWS Lambdaにアプリケーションをデプロイしたりできるものです。

Kotless自体の説明や導入方法などは以下の記事で紹介していますので、そちらをご覧ください。 blog.takehata-engineer.com

サンプルコード

前回のKotless記事を書いた際に公開していたプロジェクトに、今回のアプリケーションの入ったディレクトリを追加しています。

github.com

一部Twitter API関連で設定が必要で、必要な対応はREADMEに書いてあるので、動かしてみたい方はこちらも参考にしてください。

作ろうと思っているアプリケーション

まず、作ろうとしているアプリケーションについて簡単に説明します。

自分の過去のツイートを日ごとに閲覧できるアプリケーション(カコミエールというアプリの模倣)

カコミエールというアプリがあります。

カコミエール

カコミエール

  • Yusuke Kajiki
  • エンターテインメント
  • 無料
apps.apple.com

これは自分のTwitterで過去ツイートした全ての内容が日ごとに見れるアプリケーションです。 例えば12月22日だった場合、Twitterを開始した年から現在日時の前年までの12月17日のツイートが全部表示されます。

例)もし2018年に始めた人が、12月24日に見た場合
2018年12月24日
2019年12月24日
2020年12月24日

のツイートがリストで全部表示される。

愛用していたのですが、Twitter APIの仕様変更などの影響か、私のアカウントではツイートが表示されなくなってしまいました。
そのため自分のアカウントで説明ができないのですが、現在も使えてる方もいるようなので、画面を実際に見たい方はTwitterで #カコミエール で検索するなどしていただくと良いと思います。

2018年を最後にアップデートもされておらず、今後も使えるようになる見込みがないため、類似のアプリで少し機能を拡張したものなどを作ろうと考えました。

通常のTwitter APIでは実現が不可能だった

ただ、Twitter APIは過去1週間分のデータまでしか取得できないしようになっています。

developer.twitter.com

そのため、今回のように自分がTwitterを始めてから現在までのツイートを対象に取得することはできません。
search/universalというAPIを使うとできるらしいのですが、本来一般人が使用するようには公開されておらず、"公式のConsumerKey/ConsumerSecret"を使わないとできないということで怪しくて使いたくなく・・・

あとはstatuses/user_timelineというAPIを使うことでユーザー指定でツイートのリストを取得することはできるのですが、こちらも3200件までという制限があるため全然足りません。
名前の通り、あくまでタイムラインをリアルタイムで流す時などに使うもののようです。

Twitter APIで取得したデータを日々保存し、自分用のアプリケーションとして最小限の構成で作る

そのため別の方法を取ろうと考えました。
その方法は

  • DailyでTwitter APIを叩き、ツイートの情報をDynamoDBに保存する
  • アプリで表示するツイートはDynamoDBから取得する

というものです。
これならTwitter APIの制限も気にすることなく、使うことができます。

問題はDailyで収集し始める前のツイートをどうするかですが、これに関してはAPIではなくTwitterの画面からアーカイブをダウンロードする方法があり、これで取得したJSONファイルから登録する予定です。

help.twitter.com

実装の事前準備

実装前に必要な環境などの準備です。
基本劇な環境の構築方法は以下の記事で紹介しています。

blog.takehata-engineer.com

こちらで作成した環境に対して、追加で必要なことを今回は解説していきます。

Twitter APIの登録とAPIキー、トークンの取得

Twitter APIを使用するにあたって、登録してAPIキーとトークンを取得する必要があります。
以下のサイトなどを参考に、ご自分のTwitterアカウントで登録してください。

www.itti.jp

DynamoDBのテーブル作成

DynamoDBに、Tweetという名前で以下のテーブルを作成します。

キーの種類 物理名 意味
パーティションキー id ツイートのID(パーティションキー) 数値
ソートキー - - -

パーティションキーとしてツイートのIDをidという名前で保持しています。
Twitter APIから取得した各ツイートに振られているLong値のIDになります。
ソートキーはなしです。

中に入るデータとしては、以下のようなデータになります。

物理名 意味
id ツイートのID 数値
tweet_date ツイートした年月日 文字列
tweet_time ツイートした時分秒 文字列
tweet_text ツイートした文章 文字列

パーティションキーで指定したツイートのIDと、ツイートの年月日と時分秒、ツイートした文章を保存します。

また、グローバルセカンダリインデックスとして、datetime-indexという名前で以下を指定します。

パーティションキー ソートキー
tweet_date tweet_time

日を指定しての検索が必要になるので、日をパーティションキーとし、一意にするために時間をソートキーとして持たせています。
今回の実装はグローバルセカンダリインデックスを使った検索だけになりますが、後でidを指定しての取得も実装する予定なのと、構造として直感的なのでテーブルのパーティションキーはidにしています。

使用するいくつかのライブラリの依存関係を追加

build.gradle.ktsに、以下の依存関係を追加します。

dependencies {
    implementation("io.kotless", "kotless-lang", "0.1.7-beta-5")
    implementation("com.amazonaws:aws-java-sdk-dynamodb:1.12.126")
    implementation("org.twitter4j:twitter4j-core:4.0.7")
    implementation("dev.akkinoc.util:yaml-resource-bundle:2.1.0")
}

今回はKotless DSLを使用します。
その他の追加している依存関係は以下になります。

  • aws-java-sdk-dynamodb・・・DynamoDBを扱うためのJavaのSDK
  • twitter4j-core・・・Twitter APIを実装するTwitter4Jというライブラリ
  • yaml-resource-bundle・・・YAMLで定義した情報をJavaやKotlinのコードにバンドルするためのライブラリ

yaml-resource-bundleは、TwitterのAPIキーなどの認証で使う情報をコードから切り出してYAMLで管理したいため使用しています。

Twitterの認証で使う情報を記述した設定ファイル

src/main/resources配下に、twitter.yamlという名前で以下の内容のファイルを作成します。

consume_key: XXXXXXXXXXXXXXXXXXXXX
consume_secret: XXXXXXXXXXXXXXXXXXXXX
access_token: XXXXXXXXXXXXXXXXXXXXX
access_token_secret: XXXXXXXXXXXXXXXXXXXXX
account_name: hogehoge 

consume_keyaccess_token_secretは、前述のTwitter APIの登録後に取得した情報を記述してください。
account_nameには、取得したいご自身のTwitterのアカウント名を設定します。

Twitter4JからTwitter APIを実行する際に、これらの情報を使用します。

Dialyでツイートを収集しDynamoDBに登録する関数

ではここからコードの実装に入っていきます。
まずはDailyでツイートを収集してDynamoDBに登録する関数を作っていきます。

src/main/kotlin配下に任意の名前でKotlinのファイルを作成し、関数を追加していきます。
登録処理の全体は、以下になります。

const val TWITTER_TIME_FORMAT = "\"%d-%02d-%02d_%02d:%02d:%02d_JST\""
const val TABLE_DATE_FORMAT = "%d-%02d-%02d"
const val TABLE_TIME_FORMAT = "%02d:%02d:%02d"

val twitterConfig = ResourceBundle.getBundle("twitter", YamlResourceBundle.Control)

@DynamoDBTable("Tweet", PermissionLevel.ReadWrite)
object TweetTable {
    private val twitterClient = TwitterFactory(
        ConfigurationBuilder().setDebugEnabled(true)
            .setOAuthConsumerKey(twitterConfig.getString("consume_key"))
            .setOAuthConsumerSecret(twitterConfig.getString("consume_secret"))
            .setOAuthAccessToken(twitterConfig.getString("access_token"))
            .setOAuthAccessTokenSecret(twitterConfig.getString("access_token_secret"))
            .build()
    ).instance

    fun putTweetList(accountName: String, since: String, until: String): List<Tweet> {
        val query = Query("from:$accountName since:$since until:$until")
        val queryResults = twitterClient.search(query).tweets

        val tweetList = queryResults.map {
            Tweet(it.id, LocalDateTime.ofInstant(it.createdAt.toInstant(), ZoneId.systemDefault()), it.text)
        }
        val client = AmazonDynamoDBClientBuilder.defaultClient()
        tweetList.forEach {
            val time = it.time
            val values = mapOf(
                "id" to AttributeValue().withN(it.id.toString()),
                "tweet_date" to AttributeValue().withS(
                    TABLE_DATE_FORMAT.format(
                        time.year,
                        time.month.value,
                        time.dayOfMonth
                    )
                ),
                "tweet_time" to AttributeValue().withS(TABLE_TIME_FORMAT.format(time.hour, time.minute, time.second)),
                "tweet_text" to AttributeValue().withS(it.text)
            )
            val request = PutItemRequest().withItem(values).withTableName("Tweet")
            client.putItem(request)
        }

        return tweetList
    }

    @Scheduled("0 0 1/1 * ? *")
    private fun putTweetList() {
        val accountName = twitterConfig.getString("account_name")

        val lastDate = LocalDateTime.now(ZoneId.of("Asia/Tokyo")).minusDays(1)
        val year = lastDate.year
        val month = lastDate.month.value
        val day = lastDate.dayOfMonth

        val since = TWITTER_TIME_FORMAT.format(year, month, day, 0, 0, 0)
        val until = TWITTER_TIME_FORMAT.format(year, month, day, 23, 59, 59)

        putTweetList(accountName, since, until)
    }
}

上から順に解説していきます。

各種定数とtwitter.yamlのバンドル

まずトップレベルに記述している、定数とtwitter.yamlをバンドルしている部分です。

const val TWITTER_TIME_FORMAT = "\"%d-%02d-%02d_%02d:%02d:%02d_JST\""
const val TABLE_DATE_FORMAT = "%d-%02d-%02d"
const val TABLE_TIME_FORMAT = "%02d:%02d:%02d"

val twitterConfig = ResourceBundle.getBundle("twitter", YamlResourceBundle.Control)

TWITTER_TIME_FORMATは、Twitter APIでツイートを検索する時に使用する日時文字列のフォーマットです。
詳しくは後述しますが、yyyy-MM-dd_HH:mm:ss_JSTの形式で指定します。

TABLE_DATE_FORMATTABLE_TIME_FORMATは、DynamoDBのテーブルに登録する年月日と、時分秒のフォーマットです。

そしてtwitterConfigは、前述のtwitter.yamlで定義した情報をバンドルしています。
第一引数に、バンドルしたいYAMLのファイル名を指定しています。

DynamoDBのテーブルと関連付けるアノテーション

TweetTableというobjectを作り、@DynamoDBTableというアノテーションを付与しています。

@DynamoDBTable("Tweet", PermissionLevel.ReadWrite)
object TweetTable {

これはDynamoDBのテーブル紐付けて権限などの設定を記述するもので、引数にはテーブル名と権限のレベルを書いています。
このアノテーションを付けることにより、作成されるLambdaの関数のIAMロールに、DynamoDBへのアクセスのポリシーを一緒に追加してくれます。
ここではPermissionLevel.ReadWriteを渡しているので、AmazonDynamoDBFullAccessのポリシーがされます。

このアノテーションを設定していない場合は、自分でポリシーをアタッチしないとDynamoDBへのアクセスができずエラーになります。

Twitter APIを実行するClientの生成

objectの直下でTwitter APIを実行するClientの生成をしています。

private val twitterClient = TwitterFactory(
    ConfigurationBuilder().setDebugEnabled(true)
        .setOAuthConsumerKey(twitterConfig.getString("consume_key"))
        .setOAuthConsumerSecret(twitterConfig.getString("consume_secret"))
        .setOAuthAccessToken(twitterConfig.getString("access_token"))
        .setOAuthAccessTokenSecret(twitterConfig.getString("access_token_secret"))
        .build()
).instance

Twitter4JのTwitterFactoryというクラスを使い、Twitter APIの各種APIキーやトークンを使用して生成します。

twitterConfig.getString("xxxx")で取得しているのは、前述のtwitter.yamlで設定した値です。
ファイルのトップレベルのところでバンドルしていたtwitterConfigから、getString関数にYAMLのキー名を指定することで取得できます。

ツイートの取得

putTweetListという関数で、Twitterのアカウント名と検索対象と開始、終了日時を引数に取り、それを使ってTwitter APIを実行しています。

fun putTweetList(accountName: String, since: String, until: String): List<Tweet> {
    val query = Query("from:$accountName since:$since until:$until")
    val queryResults = twitterClient.search(query).tweets

    val tweetList = queryResults.map {
        Tweet(it.id, LocalDateTime.ofInstant(it.createdAt.toInstant(), ZoneId.systemDefault()), it.text)
    }

Queryクラスの引数に渡しているのが検索条件のクエリです。
fromでアカウント名を指定、sinceとuntilで検索対象の開始日時と終了日時を指定しています。

Twitterの期間検索には、前述の定数で設定していたyyyy-MM-dd_HH:mm:ss_JSTの形式の日時文字列が必要で、引数のsinceとuntilもこの形式で渡ってくる想定になります。
ちなみにTwitterの検索窓に同様の構文で検索すると、ユーザーと期間でツイートを検索することができます。

例)

from:n_takehata since:2021-12-17_00:00:00_JST until:2021-12-17_23:59:59_JST

そしてsearch関数の結果のオブジェクトに含まれるtweetsに各ツイートの情報のListが入っています。
この取得結果をmapで前述のTweetクラスのListに変換しています。

DynamoDBへの登録処理

続いてDynamoDBへの登録処理です。

val client = AmazonDynamoDBClientBuilder.defaultClient()
tweetList.forEach {
    val time = it.time
    val values = mapOf(
        "id" to AttributeValue().withN(it.id.toString()),
        "tweet_date" to AttributeValue().withS(
            TABLE_DATE_FORMAT.format(
                time.year,
                time.month.value,
                time.dayOfMonth
            )
        ),
        "tweet_time" to AttributeValue().withS(TABLE_TIME_FORMAT.format(time.hour, time.minute, time.second)),
        "tweet_text" to AttributeValue().withS(it.text)
    )
    val request = PutItemRequest().withItem(values).withTableName("Tweet")
    client.putItem(request)
}

return tweetList

AmazonDynamoDBClientBuilder.defaultClient()でdefaultのAWSのcredentialsで、DynamoDBへ接続するclientを生成します。
そしてTwitter APIから取得したtweetListをforEachで回し、順にDynamoDBへ登録していきます。

登録するレコードはKeyがDynamoDBのテーブルのカラム名を指定する文字列、Valueが登録する値を設定したAttributeValue型のMapで指定します。
PutItemRequestクラスに登録レコードのMapとテーブル名を指定してrequestを作成し、putItemメソッドを実行することでDynamoDBのAPIが叩かれ、登録処理が実行されます。

Dailyで前日の日付で登録処理を実行する関数

ここまでで登録処理はできましたが、さらにそれをDailyで定期実行する処理が必要です。

@Scheduled("0 0 1/1 * ? *")
private fun putTweetList() {
    val accountName = twitterConfig.getString("account_name")

    val lastDate = LocalDateTime.now(ZoneId.of("Asia/Tokyo")).minusDays(1)
    val year = lastDate.year
    val month = lastDate.month.value
    val day = lastDate.dayOfMonth

    val since = TWITTER_TIME_FORMAT.format(year, month, day, 0, 0, 0)
    val until = TWITTER_TIME_FORMAT.format(year, month, day, 23, 59, 59)

    putTweetList(accountName, since, until)
}

@ScheduledアノテーションでAWSのCron形式の文字列を渡すと、その設定でCloudWatch Eventsで関数に時間のトリガーが設定されます。
各フィールドは左から分 時 日 月 曜日 年を表しています。

ここではday(3番目の数字)に1/1と指定しています。
*?を渡している箇所はワイルドカードで、全ての値が対象となります。 /で区切った数値は左が初期値、右が増分値を表していて、この設定では日が1から1ずつの増やした数値のタイミングで、実行されます。

つまり「全ての年月で、1日から1日毎(毎日)の0時0分に実行する」という設定になります。
/の右側を例えば3にした場合は、1日から3日毎(1日、4日、7日...)という設定になります。
(UTCの0時0分なので、日本時間では毎日9時に実行されます)

Cron形式については以下の記事なども参考に読んでいただくと良いと思います。

qiita.com

@Scheduledの引数は定数が用意されているが、everyDayの値にバグがある

本来KotlessではアノテーションのScheduledクラスに、Cron形式の文字列が定数として定義されており、以下のように指定することができます。

// 毎時実行
@Scheduled(Scheduled.everyHour)
// 毎日実行
@Scheduled(Scheduled.everyDay)

しかし現在は以下のようになっていて、今回使いたいeveryDayの指定が間違っているバグがあります。

@Target(AnnotationTarget.FUNCTION)
annotation class Scheduled(val cron: String, val id: String = "") {
    @Suppress("unused")
    companion object {
        const val everyMinute = "0/1 * * * ? *"
        const val every5Minutes = "0/5 * * * ? *"
        const val every10Minutes = "0/10 * * * ? *"
        const val everyHour = "0 0/1 * * ? *"
        const val every3Hours = "0 0/3 * * ? *"
        const val everyDay = "0 0 0/1 * ? *"
    }
}

日のフィールドは本来1〜31の数値で指定しないと行けないのですが(0日は存在しないため)、初期値に0を指定してしまっています。
そのため使用するとフォーマットの不正でエラーになります。

Exception in thread "main" java.lang.RuntimeException: CronExpression '0 0 0 0/1 * ? *' is invalid.

現在は一旦PRを送ってみているので、これが取り込まれるか修正が入るかしたら、こちらの定数で指定するように変更したいと思います。 github.com

DynamoDBから特定の日の過去ツイートを取得する関数

続いてはDynamoDBから特定の日のツイートを取得する関数を解説します。
こちらも前述のTweetTableのobject内に作成します。

全体としては以下になります。

fun getTweetListByMonthDay(month: Int, day: Int): Map<Int, List<GetTweetListResponse>> {
    val twitterUser = twitterClient.showUser(twitterConfig.getString("account_name"))
    val startYear = LocalDateTime.ofInstant(twitterUser.createdAt.toInstant(), ZoneId.systemDefault()).year
    val currentYear = LocalDateTime.now().year

    val client = AmazonDynamoDBClientBuilder.defaultClient()
    val table = DynamoDB(client).getTable("Tweet")
    val index = table.getIndex("datetime-index")

    val tweetMap = mutableMapOf<Int, List<GetTweetListResponse>>()
    for (year in startYear..currentYear) {
        val date = "$year-$month-$day"

        val query = QuerySpec()
            .withProjectionExpression("id, tweet_date, tweet_time, tweet_text")
            .withKeyConditionExpression("tweet_date = :v_date")
            .withValueMap(ValueMap().withString(":v_date", date))
        val queryResults = index.query(query)

        tweetMap[year] = queryResults.map {
            GetTweetListResponse(
                it.getLong("id"),
                "${it.getString("tweet_date")} ${it.getString("tweet_time")}",
                it.getString("tweet_text")
            )
        }
    }

    return tweetMap
}

こちらも順に上から解説していきます。
これと別でレスポンスで使用するデータクラスと、ルーティングする関数が必要になりますが、そちらは後述します。

レスポンスのデータクラス

まず、取得した値をAPIのレスポンスとして返却する際に使用するデータクラスを先に作ります。

data class GetTweetListResponse(val id: Long, val time: String, val text: String)

Tweetクラスとほぼ同様ですが、timeを表示用の形式の文字列として保持しています。

Twitterの開始年と現在の年を取得

関数の引数としては月と日を受け取り、戻り値の値としてMapを返しています。

fun getTweetListByMonthDay(month: Int, day: Int): Map<Int, List<GetTweetListResponse>> {
    val twitterUser = twitterClient.showUser(twitterConfig.getString("account_name"))
    val startYear = LocalDateTime.ofInstant(twitterUser.createdAt.toInstant(), ZoneId.systemDefault()).year
    val currentYear = LocalDateTime.now().year

過去全ての年で、指定した日のツイートを取得するという仕様なので、年をKey、年毎のツイートのListをValueとしたMapを返す形になります。

対象の年ですが、まずtwitterClient.showUserでTwitter APIからユーザー情報を取得し、そのレスポンスのcreatedAtからTwitterを開始した年を取得します。
そして現在日時から、現在の年を取得します。

この「Twitterを開始した年」から「現在の年」までが対象の範囲となります。

DynamoDBのインデックスにクエリを実行するオブジェクトの生成

検索のクエリは、テーブルでなくグローバルセカンダリインデックスに対して実行するので、インデックスに対してアクセスするためのオブジェクトを作成します。

val client = AmazonDynamoDBClientBuilder.defaultClient()
val table = DynamoDB(client).getTable("Tweet")
val index = table.getIndex("datetime-index")

clientを作るところまでは登録処理と一緒ですが、そこから更にDynamoDBクラスを使用してテーブル名を取得し、テーブルのオブジェクトからインデックスのオブジェクトを取得しています。
それぞれgetTablegetIndexの引数で対象のテーブル名、インデックス名を指定することで取得できます。

年ごとにDynamoDBからデータを取得してMapに追加する処理

インデックスにクエリを実行して、取得した結果で戻り値のMapを作っていきます。

val tweetMap = mutableMapOf<Int, List<GetTweetListResponse>>()
for (year in startYear..currentYear) {
    val date = "$year-$month-$day"

    val query = QuerySpec()
        .withProjectionExpression("id, tweet_date, tweet_time, tweet_text")
        .withKeyConditionExpression("tweet_date = :v_date")
        .withValueMap(ValueMap().withString(":v_date", date))
    val queryResults = index.query(query)

    tweetMap[year] = queryResults.map {
        GetTweetListResponse(
            it.getLong("id"),
            "${it.getString("tweet_date")} ${it.getString("tweet_time")}",
            it.getString("tweet_text")
        )
    }
}

return tweetMap

Twitterを開始した年から現在の年までが対象となるため、startYear..currentYearでループを回します。
そして引数のmonthdayと結合して検索条件用の文字列dateを作ります。

クエリはQuerySpecを使用して作成します。
withProjectionExpressionでは取得するカラム名を指定し(RDBでいうSELECT句に当たる部分)、withKeyConditionExpressionでは検索条件を指定します(RDBでいうWHERE句に当たる部分)。

:v_dateとしているところはパラメータで、withValueMapで値を設定することができます。
ValueMapに対して、今回は引数の方が文字列なのでwithStringを使って指定しています。

そしてインデックスのオブジェクトのquery関数を、QuerySpecで作成したクエリのオブジェクトを引数に渡して実行すると、検索結果が取得できます。
検索結果はCollectionとして取得できるので、mapでGetTweetListResponseに変換し、yearのKeyに対してのValueとしてMapに追加します。

全部の年のツイートをMapに追加したら、それを返却して完了です。

リクエストで月日を受け取ってツイートの取得処理を呼び出すルーティングの関数

検索処理はAPIとして用意するので、最後にルーティングの関数を追加します。

@Get("/find")
fun findTweet(month: Int, day: Int) = getTweetListByMonthDay(month, day)

@GetでGETのAPIとして定義します。
Kotless DSLではルーティングの関数に引数を定義すると、クエリパラメータの値がバインディングされます。

これでクエリパタメータで月、日の値を受け取り、前述のgetTweetListByMonthDayを実行して返却するAPIができました。

plan、deployを実行して動作確認

アプリケーションの実装は完了したので、planを実行して変更内容を確認し、deployを実行してアプリケーションをデプロイします。

$ ./gradlew plan
$ ./gradlew deploy

デプロイ後放置すると、DailyでDynamoDBにツイートのデータが積まれていきます。

f:id:take7010:20211224085624p:plain

そして以下のように検索のAPIを月日を指定して実行すると、当該の日のツイートを年毎のListにしたMapが返却されます。

$ curl "https://mdibxq3aai.execute-api.us-west-2.amazonaws.com/1/find?month=12&day=17"
{2011=[], 2012=[], 2013=[], 2014=[], 2015=[], 2016=[], 2017=[], 2018=[], 2019=[], 2020=[], 
2021=[GetTweetListResponse(id=1471816801135001604, time=2021-12-17 12:17:25, 
text=満を持して強力なMacBook Proを注文した。メモリ64GBの世界がどんなもんなのか楽しみ。
ただ納品が遠い https://t.co/Q4UeiMPJI2)]}

(まだ最近のデータしか入れていないので、2021年以外は空配列で返ってきています)

改善しようと思っている点

今回のサンプルとしては一旦仕組みは作ったのですが、今後実際に使えるようにするまで以下の点は改善していこうと思っています。

  • 年と月日を別のカラムにして、月日だけで検索できるようにする
  • 検索のAPI実装はKtor DSLで別Functionとして実装

年毎にループ回して検索する仕様になっていたのですが、今回の実装ならDynamoDBに入れる時点で年と日を分けたカラムにしてしまえば月日の指定で一回のクエリで取れることに気づいたので、修正しようと思っています。
もともとTwitter APIを直接検索かける想定で作り始めていたので、それに引っ張られた仕様になってしまっていました。

そして今回は登録も検索も全部を同じLambdaのFunctionに詰め込んでいるのですが、登録のバッチはこのままKotless DSLで、検索のAPIはKtor DSLを使って別Functionにしようと考えています。
APIの実装などはKtor DSLを使っている方が、書き慣れているのもあり実装しやすいためです(規模が小さいのであんまり変わらないといえば変わらないですが)。

あとはDynamoDBに保存しておくカラムの精査や、いくつかの機能追加はしようと思います。

まとめ

今回はDynamoDB、Twitterと外部連携をしたアプリケーションをKotlessで実装してみました。
Twitter4Jのようなサードパーティライブラリなどを使うのも、Gradleに依存関係を追加するだけで通常のKotlinアプリケーションと同じような感覚で実装でき、Lambdaのアプリケーションであることをあまり意識せず作れるのがとても楽でした。

また、定期実行やIAMロールへのポリシーのアタッチなど、各種設定がKotlinのコード内で完結するのもとても便利です。

まだまだ開発途中のフレームワークですが、今後もKotlessに期待して動向を追っていきたいと思います。

【宣伝】サーバーサイドKotlinの書籍を発売しています!

私の執筆した書籍「Kotlin サーバーサイドプログラミング実践開発」が発売中です!

gihyo.jp

タイトル通り実践での開発にも持っていける内容になっているので、サーバーサイドKotlinを始めてみたい方はぜひお手にとっていただければと思います。