たかぎとねこの忘備録

プログラミングに関する忘備録を自分用に残しときます。マサカリ怖い。

Ktor + Exposed + Cloud RunでCloud SQLに接続させる方法

Cloud RunにデプロイしようとしたKtor + Exposedのアプリケーションイメージが、Cloud SQLへ接続できないことが原因で何度もデプロイに失敗してしまった。解決までに数時間要してしまい、ネットを探してもExposedを使った時のCloud SQLへの接続方法を記した記事が日本語と英語も含めてほとんど見つからなかった。なので忘備録として残しておこうと思う。

まずローカルでCloud SQL Auth Proxyを立ち上げてCloud SQLに接続する場合、次のようなURLを構築すると思う。

val jdbcURL = "jdbc:postgresql://0.0.0.0:3306/postgres"

これに倣って、Cloud SQLの公開IPアドレスと取り替えてCloud SQLに接続しようとしてみた。

val jdbcURL = "jdbc:postgresql://公開IPアドレス/postgres"

次のようなエラーが発生した。

org.postgresql.util.PSQLException: The connection attempt failed. at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:331) at

Cloud RunからCloud SQLに接続する方法として、TCPで接続する方法とUnixドメインソケットを使って接続する方法がある。各URLのフォーマットは次の通り。

TCPで接続する場合

val jdbcURL = "jdbc:postgresql:///$databaseName?cloudSqlInstance=$instanceConnectionname&socketFactory=com.google.cloud.sql.postgres.SocketFactory&user=$user&password=$password"

UNIXドメインソケットを使って接続する場合

val jdbcURL = "jdbc:postgresql:///$databaseName?unixSocketPath=$unixSocketPath&cloudSqlInstance=$instanceConnectionname&socketFactory=com.google.cloud.sql.postgres.SocketFactory&user=$user&password=$password"

Unixドメインソケットを使って接続する場合は、unixSocketPathクエリーパラメーターを渡す必要があるので注意する。

各変数にどんな値を代入すれば良いかは次の通り。

  • databaseName
    • 接続したいデータベース名
      • postgres (例
  • instanceConnectionName
    • Cloud SQLインスタンスの概要ページに表示されている接続名のフィールドの値
      • プロジェクト名:リージョン名:データベースのインスタンス名のフォーマット
  • socketFactory
    • cloud-sql-jdbc-socket-factoryライブラリにあるクラス。
      • com.google.cloud.sql.postgres.SocketFactoryを渡す。
  • unixSocketPath
    • ソケットへのパス
      • /cloudsql/接続名(instanceConnectionName変数と同じ値)
  • user
    • データベースのユーザー名
  • password
    • データベースのパスワード

Socket Factoryを使えるようにするためにcloud-sql-jdbc-socket-factoryを依存関係として追加する。

github.com

dependencies {
    implementation("org.postgresql:postgresql:$postgresql_version")
    ...
    implementation("com.google.cloud.sql:postgres-socket-factory:1.7.2")
}

依存関係を追加しないでクラス名だけ指定した場合は次のようなエラーが発生する。

org.postgresql.util.PSQLException: The SocketFactory class provided com.google.cloud.sql.postgres.SocketFactory could not be instantiated. at org.postgresql.core.SocketFactoryFactory.getSocketFactory(SocketFactoryFactory.java:43) at org....

実際のコードは次の通り。

object DatabaseFactory {
    fun init(
        user: String = "",
        password: String = "",
        databaseName: String = "",
        instanceConnectionName: String = "",
        unixSocketPath: String = "",
    ) {
        val driverClassName = "org.postgresql.Driver"
        val jdbcURL = "jdbc:postgresql:///$databaseName?unixSocketPath=$unixSocketPath&cloudSqlInstance=$instanceConnectionName&socketFactory=com.google.cloud.sql.postgres.SocketFactory&user=$user&password=$password"
        val database = Database.connect(
            url = jdbcURL,
            driver = driverClassName,
            user = user,
            password = password,
        )
    ....
    }
}

各変数の値は環境変数として展開して渡すようにする。そのために、application.confを設定する。

google {
    cloud_sql {
        username = "takagimeow"
        username = ${?DB_USER_NAME}
        password = "takagimeow_pass"
        password = ${?DB_USER_PASS}
        path = "postgres"
        path = ${?DB_PATH}
        database_name = ""
        database_name = ${?DATABASE_NAME}
        instance_connection_name = ""
        instance_connection_name = ${?INSTANCE_CONNECTION_NAME}
        unix_socket_path = ""
        unix_socket_path = ${?UNIX_SOCKET_PATH}
    }
}

設定した環境変数Application.module()で取得し、DatabaseFactory.init()の呼び出し時に渡す。

fun Application.module() {
    DatabaseFactory.init(
        user = environment.config.property("google.cloud_sql.username").getString(),
        password = environment.config.property("google.cloud_sql.password").getString(),
        databaseName = environment.config.property("google.cloud_sql.database_name").getString(),
        instanceConnectionName = environment.config.property("google.cloud_sql.instance_connection_name").getString(),
        unixSocketPath = environment.config.property("google.cloud_sql.unix_socket_path").getString()
    )
    ...
}

それでも接続できない場合

Cloud SQL Admin APIが有効になっていない可能性があるので、必ず有効になっているかを確認する。

NestJS + Prisma + Cloud Run + Cloud SQLを試す

ソケットパスに関して公式ドキュメントでは次の様に書かれている。

注: PostgreSQL スタンダードでは、ソケットパスに .s.PGSQL.5432 接尾辞が含まれている必要があります。一部のライブラリではこの接尾辞が自動的に適用されますが、他のライブラリでは、ソケットパスを以下のように指定する必要があります。

なので、一度/.s.PGSQL.5432をソケットパスの末尾につけて試してみる。

Connect from App Engine standard environment  |  Cloud SQL for PostgreSQL  |  Google Cloud

参考

App Engine から Cloud SQL に接続する - albatrosary's blog

クイックスタート: Cloud Run から Cloud SQL for PostgreSQL に接続する  |  Google Cloud

Cloud Run から接続する  |  Cloud SQL for PostgreSQL  |  Google Cloud

インスタンスの作成  |  Cloud SQL for PostgreSQL  |  Google Cloud

Cloud Run を徹底解説! - G-gen Tech Blog

Identity-Aware Proxy(IAP)とCloud Armorを使用してCloud Runサービスへのアクセス制御を実装する - G-gen Tech Blog

Cloud SQL Auth Proxy を使用して Cloud Run から Cloud SQL に接続する - G-gen Tech Blog

Issue connecting to Google Cloud PostgreSQL from GAE project · Issue #136 · GoogleCloudPlatform/cloud-sql-jdbc-socket-factory · GitHub

Can't connect Cloud Data Fusion with Google Cloud SQL for PostgreSQL · Issue #134 · GoogleCloudPlatform/cloud-sql-jdbc-socket-factory · GitHub

https://kaleidot.net/ktor-ktor-%E3%81%A8-postgres-%E3%82%92%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%81%A6%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B-c9a2b85353a4

ktor - How connect tot Google Cloud SQL via Exposed - Stack Overflow

I ve been using unix domain sockets in mac with ktor It s be | Ktor | Kotlinlang

Node.js+SequelizeでCloud RunからCloud SQLへ接続する方法

Java を使用してソケット接続を作成する  |  Cloud SQL for PostgreSQL  |  Google Cloud

Google CloudSQL インスタンスに接続する | DataGrip

AndroidアプリでJetpack Composeを使って使用しているライブラリのライセンスを表示してみる

Androidアプリを開発していると直面するライセンス表示問題。

色々調べるとCookpadさんが公開しているプラグインにたどり着いた。だが、お察しのとおりすでにアーカイブされている。

github.com

代わりにおすすめされているのが、Googleが公開しているOSS Licenses Gradle Plugin

play-services-plugins/oss-licenses-plugin at master · google/play-services-plugins · GitHub

残念ながらJetpack Composeには現時点で対応していない。僕はJetpack Composeを使ってライセンスを表示したい。

そこで、見つけたのがJumpei Matsudaさんが公開しているlicense-list-plugin

github.com

このプラグインを使ってartifact-definition.ymlを生成し、そのデータをパースした内容をCompose可能な関数で表示してみる。

アプリのbuild.gradleにて、プラグインの設定を行う。

plugins {
    id 'com.android.application'
    id "io.github.jmatsu.license-list"
}

// Licensing
apply plugin: "com.android.application"
apply plugin: "io.github.jmatsu.license-list"

ルートディレクトリにあるプロジェクトのbuild.gradleにbuildscriptの設定を追加する。

buildscript {
    ext {
        license_list_gradle_version = '0.8.1'
    }
    dependencies {
        classpath "io.github.jmatsu:license-list-gradle:$license_list_gradle_version"
    }
}

追加が完了したら、実際にプラグインを実行する。

./gradlew initLicenseList

実行後、app/artifact-definition.ymlapp/license-catalog.ymlが作成されているはず。

モジュールを右クリックして、New -> Folder -> Assets Folderの順番で選択していき、assetsフォルダを作成する。 そのassetsの中に先ほど生成されたartifact-definition.ymllicense-catalog.ymlを移動する。

artifact-definition.ymlの中身をパースするために、kamlライブラリを導入する。

github.com

build.gradleに次の設定と依存関係を追加する。

plugins {
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.4.20'
}

dependencies {
    implementation "com.charleskorn.kaml:kaml:0.49.0" // Get the latest version number from https://github.com/charleskorn/kaml/releases/latest
}

assets/artifact-definition.ymlを実際に読み込むために、Contextインスタンスを使用する。

@kotlinx.serialization.Serializable
data class License(
    val release: Map<String, List<ArtifactDefinition>>,
    val test: Map<String, List<ArtifactDefinition>>,
    val androidTest: Map<String, List<ArtifactDefinition>>,
)

@kotlinx.serialization.Serializable
data class ArtifactDefinition(
    val key: String,
    val displayName: String,
    val url: String?,
    val copyrightHolders: List<String>,
    val licenses: List<String>
)

fun parseLicense(context: Context) {
    val yamlStr = context.assets.open("artifact-definition.yml").bufferedReader().use {
        it.readText()
    }
    val result = Yaml.default.decodeFromString(License.serializer(), yamlStr)
}

ひとつのライセンス要素を表示するためのCompose可能な関数を定義する。

@Composable
fun LicenseItem(
    displayName: String,
    url: String?,
    copyrightHolders: List<String>,
    licenses: List<String>,
) {
    Column() {
        Text(
            text = displayName,
            style = MaterialTheme.typography.titleLarge
        )
        Spacer(modifier = Modifier.height(8.dp))

        if (url != null) {
            Text(
                text = url ?: ""
            )
            Spacer(modifier = Modifier.height(8.dp))
        }

        if (copyrightHolders.isNotEmpty()) {
            copyrightHolders.forEach { copyrightHolder ->
                Text(
                    text = copyrightHolder
                )
            }

            Spacer(modifier = Modifier.height(8.dp))
        }

        licenses.forEach { license ->
            Text(
                text = license
            )
        }
    }
}

そして、先ほど読み取ったデータを使い、LazyColumnを呼び出してリスト形式で表示する。

LazyColumn(
    modifier = Modifier
        .fillMaxWidth()
        .fillMaxHeight()
) {
    if (license != null) {
        items(license.release.keys.toList()) { groupKey ->
            val group = license.release[groupKey]
            group?.forEach { groupItem ->
                LicenseItem(
                    displayName = groupItem.displayName,
                    url = groupItem.url,
                    copyrightHolders = groupItem.copyrightHolders,
                    licenses = groupItem.licenses
                )

                Spacer(modifier = Modifier.height(24.dp))
            }
        }

        items(license.test.keys.toList()) { groupKey ->
            val group = license.release[groupKey]
            group?.forEach { groupItem ->
                LicenseItem(
                    displayName = groupItem.displayName,
                    url = groupItem.url,
                    copyrightHolders = groupItem.copyrightHolders,
                    licenses = groupItem.licenses
                )

                Spacer(modifier = Modifier.height(24.dp))
            }
        }

        items(license.androidTest.keys.toList()) { groupKey ->
            val group = license.release[groupKey]
            group?.forEach { groupItem ->
                LicenseItem(
                    displayName = groupItem.displayName,
                    url = groupItem.url,
                    copyrightHolders = groupItem.copyrightHolders,
                    licenses = groupItem.licenses
                )

                Spacer(modifier = Modifier.height(24.dp))
            }
        }
    }
}

実際の画面。

参考

Read a Text asset(text file from assets folder) as a String in Kotlin (Android) - Stack Overflow

以前の画面の状態が意図せぬ形で復元されてしまう場合の対処法

次の様な画面遷移図のアプリを考えてみる。

今までは全ての遷移時に次の様なコードを実行していた。

fun navigate(
    destination: TnaNavigationDestination,
    route: String? = null,
    from: NavBackStackEntry? = navController.currentBackStackEntry
) {
    if (from != null && from.lifecycleIsResumed()) {
        navController.navigate(route ?: destination.destination) {
            popUpTo(navController.graph.findStartDestination().id) {
                saveState = true
            }
            launchSingleTop = true
            restoreState = true
        }
    }
}

この場合、FeedByLabelScreenからnavController.popBackStack()を呼び出したときに、FeedScreenまで遡ってしまう問題に直面した。

ここでは、四角で囲まれている画面時における戻る操作でのみFeedScreenまで遡ってほしいので、そのナビゲーション時のみpopUpToを呼び出したい。

そして他にも、一度AddCountTrainingActivityScreenからFeedScreenに戻り、ボトムナビゲーションのFeedScreenに紐づいているアイコンをタップすると、今度はAddCountTrainingActivityScreenの直前の画面であるLabelListScreenに戻ってしまう問題も発生していた。

なので、LabelListScreenから四角で囲まれている画面への遷移時のみ次のコードを実行するようにした。

fun navigateAndPopUp(
    destination: TnaNavigationDestination,
    route: String? = null,
    from: NavBackStackEntry? = navController.currentBackStackEntry
) {
    if (from != null && from.lifecycleIsResumed()) {
        val newRoute = when (destination) {
            is TopLevelDestination -> {
                destination.destination
            }
            else -> {
                route ?: destination.route
            }
        }
        navController.navigate(newRoute) {
            popUpTo(navController.graph.findStartDestination().id) {}
            launchSingleTop = true
        }
    }
}

意図せぬ画面状態の復元を避けるために、saveState=truerestoreState=trueを削除した。

そして、他の画面の遷移時には次のコードを実行する様にした。

fun navigate(
    destination: TnaNavigationDestination,
    route: String? = null,
    from: NavBackStackEntry? = navController.currentBackStackEntry
) {
    if (from != null && from.lifecycleIsResumed()) {
        navController.navigate(route ?: destination.destination) {
            launchSingleTop = true
        }
    }
}

参考

Navigation ComposeのNavOptions - Kenji Abe - Medium

Androidのスプラッシュスクリーンで簡単に背景色をダークモード対応させる方法

Android Studioで、resディレクトリを右クリックして、New -> Android Resource Directoryを選択する。

Directory Nameにvalues-nightを入力して、OKをクリックする。

values-nightディレクトリが表示されない場合は、Projectビューに切り替える。 values-nightディレクトリを見つけたら、右クリックして、New -> Values Resource Fileを選択する。

入力フィールドにcolorsを入力する。

作成されたvalues-night/colors.xmlcolor要素を追加する。nameにはactivity_backroundを指定する。値は#1f1f1f

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="activity_background">#1f1f1f</color>
</resources>

もともと存在していたvalues/colors.xmlにはライトモードに対応するactivity_backroundカラーを追加する。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="activity_background">#FFFFFF</color>
    ...
</resources>

AndroidManifest.xmlを開いて、application要素のandroid:themeに設定されているテーマを確認する。

<application
    ...
    android:theme="@style/Theme.TrainingNoteApplication"
>

該当のテーマが定義されているtheme.xmlを開いて、アプリケーション全体の背景色を変更するために次の要素を追加する。

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <style name="Theme.TrainingNoteApplication" parent="android:Theme.Material.Light.NoActionBar">
        <item name="android:windowBackground">@color/activity_background</item>
    </style>
</resources>

これでシステムのダークモードの設定に合わせて、スプラッシュスクリーン表示時の背景色が自動的に変更される。

参考

Splash screens  |  Android Developers

スタイルとテーマ  |  Android デベロッパー  |  Android Developers

DarkTheme対応のリソース設計 - DMM inside

Androidでダークテーマを試す. ダークテーマの設定方法について紹介します。 | by Keisuke Kawajiri | URL Memo | Medium

Androidではデータベースに画像のリソースIDを保存してはいけないらしい!

Androidアプリをコンパイルした後だったり、新しい画像をResource Managerに追加した後に、アプリで表示されていた画像がまったく異なる他の画像に置き換えられているケースが多々あった。

そろそろ対処しないとなーと思って色々調べていたら次のコメントを我々の拠り所スタックオーバーフローにて見つけてしまった。

Don't do this. There's a major problem with storing image ids in the database- the numbers aren't stable. Each time you recompile the app, the values can change. If you add or remove any resource, the entire mapping can change. It isn't a stable method of storing that data. Using the drawable's name and converting to id would be more stable.

– Gabe Sechan Mar 24, 2017 at 6:46 android - Storing image resource id in Sqlite database and retrieving it in int array - Stack Overflow

つまり、アプリを再コンパイルするたびにリソースIDの値は変わる可能性があるし、リソースを追加したり削除したりすると、マッピング全体が変わってしまう可能性がある。なので、データを保存する方法として、リソースIDをそのままデータベースに保存するのは誉められたものではないとのこと。

ではどうすれば良いのかというと、ここで推奨されているのはリソースのIDではなく、エントリー名を保存する方法。

他のスタックオーバーフローでも推奨されていた。

Best way to store Resource id in Database on Android - Stack Overflow

早速実装してみる。 まず、DrawableConverterインターフェースを実装する。

interface DrawableConverter {
    fun getResourceIdByEntryName(entryName: String): Int
    fun getResourceEntryNameByResourceId(resourceId: Int): String
}

getResourceIdByEntryName()では、リソースのエントリー名を受け取ってリソースIDに変換する。

getResourceEntryNameByResourceId()では、リソースIDを受け取ってエントリー名に変換する。

次にDrawableConverterImplクラスを実装する。

class DrawableConverterImpl @Inject constructor(
    @ApplicationContext private val context: Context,
) : DrawableConverter {
    override fun getResourceIdByEntryName(entryName: String): Int {
        return context.resources.getIdentifier(entryName, "drawable", context.packageName)
    }
    override fun getResourceEntryNameByResourceId(resourceId: Int): String {
        return context.resources.getResourceEntryName(resourceId)
    }
}

バインドを行う。

@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
    ...

    @Binds
    abstract fun bindDrawableConverter(
        drawableConverter: DrawableConverterImpl
    ): DrawableConverter
}

そしてリポジトリにて、DrawableConverterの注入を行い、DAOから受け取ったエンティティをリポジトリ内でモデルに変換し、ViewModelなどでその内容を受け取るようにする。

@Singleton
class AlbumRepositoryImpl @Inject constructor(
    private val albumDao: AlbumDao,
    private val drawableConverter: DrawableConverter,
) : AlbumRepository {
    override suspend fun getAlbums(
        ioDispatcher: CoroutineDispatcher
    ): Result<List<Album>> {
        return withContext(ioDispatcher) {
            val albums = albumDao.getAlbums()
            Result.Success(albums.first().map {
                val imageResourceId = drawableConverter.getResourceIdByEntryName(it.imageEntryName)
                it.asExternalModel(imageResourceId)
            })
        }
    }

    override suspend fun insertAlbum(
        title: String,
        artist: String,
        imageResourceId: Int,
        ioDispatcher: CoroutineDispatcher
    ) {
        val entryName = drawableConverter.getResourceEntryNameByResourceId(imageResourceId)
        val albumEntity = getNewAlbumEntry(
            albumTitle = title,
            albumArtist = artist,
            albumImageEntryName = entryName,
        )
        return withContext(ioDispatcher) {
            albumDao.insert(albumEntity)
        }
    }
   ...
}

図解してみると、こんな感じ。

これにより、データベースに保存するときはDAOを経由してエントリー名を渡し、画面上に表示するときはリポジトリ内で受け取ったエントリー名をリソースIDに変換してからViewModelに渡すことができる。

その結果、リソースIDの意図せぬ変更を恐れることなく、データベースに対して画像関連の値を保存できるようになった。めでたしめでたし。

Kotlinでプロジェクト内のディレクトリにある複数のJSONファイルをまとめて処理してみる

次の様なディレクトリの構成を考えてみる。

  • a_lot_of_settings
    • a
    • b
    • c
    • main
      • kotlin
        • com
          • takagimeow
            • main.kt

a_lot_of_settingsディレクトリの中にはaディレクトリとbディレクトcディレクトリが存在し、それぞれのディレクトリの中にsettings.json`が存在する。

このsettings.jsonをそれぞれ読み取り処理をしたい。

settings.jsonの中身は次のようなフォーマットになっている。

{
  "name": "takagimeow"
}

このフォーマットと一致するdata classを実装する。

data class Settings(
    val name: String,
)

JSONのパースライブラリとして、Jacksonを使用する。

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule

...

val mapper = jacksonObjectMapper()
mapper.registerKotlinModule()

File()のコンストラクタに渡す相対パスは、実行しようとしているmain()関数が記述されたファイルの場所が起点ではなく、プロジェクトのルートディレクトリが起点となるので注意が必要。

File("./a_lot_of_settings").walk().forEach { dir ->
    if (dir.isDirectory && dir.toString() != "./a_lot_of_settings") {
        val jsonString: String = File("$dir/settings.json").readText(Charsets.UTF_8)
        val result: Settings = mapper.readValue(jsonString, Settings::class.java)
        println("name: ${result.name}")
    }
}

dirには./a_lot_of_settings自身も格納されてしまうので弾いている。

最終的な実装は次の様な感じ。

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import java.io.File

data class Settings(
    val name: String,
)

suspend fun main() {
    val mapper = jacksonObjectMapper()
    mapper.registerKotlinModule()
    File("./a_lot_of_settings").walk().forEach { dir ->
        if (dir.isDirectory && dir.toString() != "./a_lot_of_settings") {
            val jsonString: String = File("$dir/settings.json").readText(Charsets.UTF_8)
            val result: Settings = mapper.readValue(jsonString, Settings::class.java)
            println("name: ${result.name}")
        }
    }
}

参考

Read a Json file with Kotlin

Kotlin - ディレクトリのすべてのファイルのリスト出力

1つのPostgreSQLコンテナに複数のデータベースを作成して、複数のアプリケーションからアクセス可能にしてみる

docker-composeでPostgreSQLコンテナを立ち上げる。docker-compose.ymlの内容は次のような感じで、Ktorプロジェクトのルートディレクトリに作成する。

version: "3.7"
services:
  postgres:
    image: 'postgres:13'
    container_name: takagimeow-postgres
    restart: always
    environment:
      - POSTGRES_USER=takagimeow
      - POSTGRES_PASSWORD=takagimeow_pass
    ports:
      - '5432:5432'
    volumes:
      - postgresql-data-vol:/var/lib/postgresql/data:rw
      - ./postgres-init:/docker-entrypoint-initdb.d

volumes:
  postgresql-data-vol:

複数のデータベースを1つのコンテナに対して作成するにあたって、volumes./postgres-init:/docker-entrypoint-initdb.dを指定している。これにより同じ階層にあるpostgres-initディレクトリにコンテナの初回起動時に実行して欲しいsqlファイルを配置することができる。

CREATE DATABASE serverside;
CREATE DATABASE clientside;

コンテナを立ち上げる。

docker-compose up -d

TablePlusを使って接続のテストを行ってみる。

まずはclientsideデータベースに対して。

次はserversideデータベースに対して。

ExposedとPostgreSQLJDBCドライバを使って接続を行う

gradle.propertiesにて、exposed_versionpostgresql_versionを定義する。

exposed_version = 0.36.2
postgresql_version = 42.5.0

build.gradleにてPostgreSQLへの接続に必要な依存関係を追加する。

val exposed_version: String by project
val postgresql_version: String by project
...
dependencies {
    implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
    implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
    implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
    implementation("org.postgresql:postgresql:$postgresql_version")
}

DatabaseFactoryを定義する。

object DatabaseFactory {
    fun init() {
        val driverClassName = "org.postgresql.Driver"
        val jdbcURL = "jdbc:postgresql://localhost:5432/serverside"
        val user = "takagimeow"
        val password = "takagimeow_pass"
        val database = Database.connect(
            url = jdbcURL,
            driver = driverClassName,
            user = user,
            password = password
        )
        transaction(database) {
            SchemaUtils.create(Users)
        }
    }

    suspend fun <T> dbQuery(block: suspend () -> T): T =
        newSuspendedTransaction(Dispatchers.IO) { block() }
}

ExposedのDSL APIusersテーブルを定義する。

object Users : Table() {
    val id = integer("id").autoIncrement()
    val email = varchar("email", 128)
    val password = varchar("password", 128)
    val name = varchar("name", 128)
    val roleType = varchar("role_type", 128)

    override val primaryKey = PrimaryKey(id)
}

Application.module拡張メソッドの先頭で、DatabaseFactory.init()を呼び出す。これにより、データベースへの接続を可能にしておく。

fun Application.module() {
    DatabaseFactory.init()

    configureKoin()
    configureSerialization()
    configureMonitoring()
    configureHTTP()
    configureSecurity()
    configureRouting()
}

Ktorアプリケーションを起動後、TablePlusを使ってserversideデータベースの中身を確認してみる。usersテーブルが作成されているのがわかる。

Prismaを使ってclientsideデータベースに接続を行う

schema.prismaは次のようする。こちらはRemix Blues Stackで使われているschema.prismaと同じ内容になっているので、試したい方はぜひRemix Blues Stackを使ってプロジェクトを作成してみてほしい。

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id    String @id @default(cuid())
  email String @unique

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  password Password?
  notes    Note[]
}

model Password {
  hash String

  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  userId String @unique
}

model Note {
  id    String @id @default(cuid())
  title String
  body  String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  userId String
}

.envを設定する。

DATABASE_URL="postgresql://takagimeow:takagimeow_pass@localhost:5432/clientside"

定義したスキーマをデータベースに反映させる。

npx prisma db push

clientsideデータベースの中身を確認してみる。スキーマで定義されているテーブルが作成されていることが確認できる。

参考

https://ysuzuki19.github.io/post/docker-mysql-postgres-multiple-databases

https://kaleidot.net/ktor-ktor-%E3%81%A8-postgres-%E3%82%92%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%81%A6%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B-c9a2b85353a4

GitHub - remix-run/blues-stack: The Remix Stack for deploying to Fly with PostgreSQL, authentication, testing, linting, formatting, etc.