takeda_san’s blog

KotlinとVRを頑張っていく方向。

SpringでAthenaを使おう!

きっかけ

Athena便利ですよね。
S3にCSV形式とかのファイルを置いて、Glue crawler実行するだけで集計できるようになっちゃうんですよ。
ディレクトリを日付ごとに分けることで、パーティションも自由自在!
こんなに簡単にログ集計できていいんですかね。

でもでも、取得したデータをちょこっと複雑なビジネスロジックで変換して出力したいこと…ありますよね。
あー、Kotlinで処理が書けたらなぁ…
というわけで、SpringでAthenaを使おう!

使おう

今回のコードはここにあります。

github.com

そして、コードは、ここを参考にしました。
なんでもブログにあって、クラメソ殿しゅごい。
SpringBootを使ってAWS Athenaへ接続してみる | DevelopersIO

でも少し書き換えが必要

でも、ちょっと前の記事なので書き直しが必要なところがあるのですよね。
ここのJDBC移行ガイドある通り、JDBC Driverのクラス名が変わったのですよね。
JDBC ドライバーを介した Athena の使用 - Amazon Athena

旧 -> com.amazonaws.athena.jdbc.AthenaDriver
新 -> com.simba.athena.jdbc.Driver

なので、application.ymlはこんな感じになります。

spring:
  datasource:
    driver-class-name: com.simba.athena.jdbc.Driver
    url: jdbc:awsathena://athena.ap-northeast-1.amazonaws.com:443;s3_staging_dir=s3://takeda-athena-results

athena:
  database: takedaathena

URLは jdbc:awsathena://AwsRegion={REGION} みたいな感じでも書けるみたいですね。
こっちのほうが短くて簡潔かな。
URLにパラメータの指定をするときの記号も

旧 -> & and ?
新 -> ;

に変わってるみたいです。

便利で便利なDefaultAWSCredentialsProviderChain

クラメソ殿の記事だと認証まわりにProfileCredentialsProviderを使っていて、ローカルではPorfile、本番ではRoleを使ったりのように切り替えたいときに、ちょっと困る。
こんなときにDefaultAWSCredentialsProviderChainを指定するとよしなにやってくれるので、非常に便利でした。

この記事読んでマ!ってなりました。
AWS SDK for Java がデフォルトで参照する credential - Qiita

優先順に認証情報を探して、適用してくれるのです。
なので、Java Configはこんな感じになってます。

@Configuration
class AthenaConfig {
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource")
    fun dataSourceProperties() = DataSourceProperties()

    @Bean("dataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource")
    fun dataSource(): SimpleDriverDataSource {
        val settings = dataSourceProperties()
        val dataSource = SimpleDriverDataSource()

        dataSource.apply {
            @Suppress("UNCHECKED_CAST")
            setDriverClass(Class.forName(settings.determineDriverClassName()) as Class<out Driver>)
            url = settings.determineUrl()

            // 認証設定を追加する
            val connectionProperties = Properties()
            connectionProperties.setProperty(
                    "aws_credentials_provider_class",
                    "com.simba.athena.amazonaws.auth.DefaultAWSCredentialsProviderChain"
            )
            setConnectionProperties(connectionProperties)
        }

        return dataSource
    }

    @Bean("jdbcTemplate")
    fun jdbcTemplate(
            @Autowired
            @Qualifier("dataSource")
            dataSource: SimpleDriverDataSource
    ): JdbcTemplate = JdbcTemplate(dataSource)
}

ここ、なんか汚くて怒られそうですが

@Suppress("UNCHECKED_CAST")
setDriverClass(Class.forName(settings.determineDriverClassName()) as Class<out Driver>)
url = settings.determineUrl()

普通のSpringのDataSourceのままだと、好きなプロパティが差し込めないのでSimpleDataSourceを使ってDefaultAWSCredentialsProviderChainを差し込んでます。
うーん、専用のクラス作ったほうが早いかな。

クエリの発行

無事設定も終わったので、JdbcTemplateを使ってクエリを発行します。

    fun selectSomeByDate(date: LocalDate): List<Person> {
        val sql = """
            SELECT
             name,
             age
            FROM
             $database.some
            WHERE
             some.year = '${date.year}'
             AND some.month = '${date.monthValue.toString().padStart(2, '0')}'
             AND some.day = '${date.dayOfMonth.toString().padStart(2, '0')}'
            ;
        """.trimIndent()

        return jdbcTemplate.query(sql) { resultSet, _ ->
            Person(
                    name = resultSet.getString("name"),
                    age = resultSet.getInt("age")
            )
        }
    }

SQLライクで使いやすいですねぇ。
結果

Person(name=hoge, age=11)

やったね。
これでログ集計し放題!

困ること

無事、Springの世界にデータを持ってこれたのですが、実際の本番ログとかって量が膨大でメモリ上に載りきらないことがありそうですね。
こんな場合ってどうするんでしょ。
Athenaで引っ張ってきたデータを全部リストに格納したいとかってありそうですが…?
なんか随時ファイルに書き込んでメモリ上から消してくとかあるんですかねぇ。