たかぎとねこの忘備録

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

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.

ExposedでEnumを使ってみる

PostgreSQLをデータベースと使用しているときに、一部のカラムをEnumとして表現したい。

そのためにはまず、Enumを型として定義する必要があるので作成してみる。これはテーブルの定義よりも前にやっておく。

こんな感じ。

class PGEnum<T : Enum<T>>(enumTypeName: String, enumValue: T?) : PGobject() {
    init {
        value = enumValue?.name
        type = enumTypeName
    }
}

カラムの値として受け取りたい値をenum classを使って定義する。

enum class Service {
    INSTAGRAM,
    TWITTER,
}

この二つを使って、テーブルを定義するとこんな感じになる。

第三引数と第四引数では、いわゆるfrom/to変換関数を渡している。

object Posts : UUIDTable() {
    val service = customEnumeration("service", "ServiceEnum", { value -> Service.valueOf(value as String) }, { PGEnum("ServiceEnum", it) })
}

SchemaUtils.create()を呼び出すときにexec()メソッドを呼び出して、CREATE TYPEを実行することで、新しいデータ型を定義する。

CREATE TYPEで指定するのは、customEnumeration()の呼び出し時にsql引数として渡した引数と同じ値。ここではServiceEnumを指定している。

AS ENUMで指定するのは、enum classで定義した内容と同じ値。

val database = Database.connect(
    url = jdbcURL,
    driver = driverClassName,
    user = user,
    password = password
)
transaction(database) { 
    exec(
        "DO $$ BEGIN " +
        "CREATE TYPE ServiceEnum AS ENUM ('INSTAGRAM', 'TWITTER'); " +
      "EXCEPTION " +
        "WHEN duplicate_object THEN null; " +
      "END $$;"
    )
  SchemaUtils.create(Posts)
}

ここで、注意しないといけないのが例外処理を行なっている点。

はじめて実行するときは、データ型がまだ定義されていない状態なので次のように書いたとしてもエラーは発生しない。

exec("CREATE TYPE ServiceEnum AS ENUM ('INSTAGRAM', 'TWITTER');")

ところが、2回目以降になると次のようなエラーが発生してしまう。

Exception in thread "main" org.jetbrains.exposed.exceptions.ExposedSQLException: org.postgresql.util.PSQLException: ERROR: type "serviceenum" already exists

これはすでに存在するデータ型を作成しようとしたために発生した例外。 なので、上記のコードではEXCEPTIONを使って例外処理を行いエラーの発生を制御している。

参考

DataTypes · JetBrains/Exposed Wiki · GitHub

Check if a user-defined type already exists in PostgreSQL - Stack Overflow

Exposed(DAO/DSL) で PostgreSQL からあれこれ SELECT するサンプル・覚書 - インドカレーファンクラブ

KtorとExposedの環境でちょっとデータベースにシードさせたいときのTips

Ktorでデータベースを使用したいので、com.takagimeow.infrastructure.database.daoDatabaseFactoryを実装してみます。

package com.takagimeow.infrastructure.database.dao

import com.takagimeow.infrastructure.database.models.Users
import kotlinx.coroutines.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.*
import org.jetbrains.exposed.sql.transactions.experimental.*

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() }
}

Application.ktで、このオブジェクトのinit()メソッドを呼び出すことでデータベースへ接続を行います。

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

そしてUsersDSLで定義します。

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)
}

このとき、ユーザーの作成に関するルートは公開したくないけども、初期データとしてユーザーレコードをテーブルに登録したい場合を考えてみました。

プロジェクトではKoinを使いたいのでUserDaoFacadeインターフェースを作成します。

package com.takagimeow.infrastructure.database.dao

import com.takagimeow.infrastructure.database.models.*

interface UserDAOFacade {
    suspend fun addNewUser(email: String, password: String, name: String): Unit
}

そして、その実装であるUserDaoFacadeImplクラスを実装します。

package com.takagimeow.infrastructure.database.dao.impl

import com.takagimeow.infrastructure.database.dao.UserDAOFacade
import com.takagimeow.infrastructure.database.dao.DatabaseFactory.dbQuery
import com.takagimeow.infrastructure.database.models.Users
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.*

class UserDAOFacadeImpl : UserDAOFacade {
    override suspend fun addNewUser(email: String, password: String, name: String): Unit = dbQuery {
        val insertStatement = Users.insert {
            it[Users.email] = email
            it[Users.password] = password
            it[Users.name] = name
            it[roleType] = "USER"
        }
        insertStatement.resultedValues?.singleOrNull()?.let(::resultRowToUser)
    }
}

実際は、各リポジトリでこのクラスを注入することでデータベースに対する操作を行なっていくのですが、今回はこのUserDAOFacadeImplを再利用してデータの挿入を行おうと思います。

com.takagimeow.infrastructure.databaseパッケージにseedパッケージを作成します。

このパッケージの中に、InserUser.ktを作成します。 そしてmain()を定義して、コードブロックの中でUserDaoFacadeImplクラスのaddNewUser()を呼び出します。

package com.takagimeow.infrastructure.database.seed

import com.takagimeow.infrastructure.database.dao.DatabaseFactory
import com.takagimeow.infrastructure.database.dao.impl.UserDAOFacadeImpl

suspend fun main() {
    DatabaseFactory.init()
    val dao = UserDAOFacadeImpl()
    dao.addNewUser(
        "takagimeow@example.com",
        "password",
        "takagi",
    )
}

あとは、IntelliJ IDEAで表示されているmain()の横の実行ボタンを押すだけ。

ターミナルには次の様に表示されました。

2022-11-16 02:15:03.836 [DefaultDispatcher-worker-3] DEBUG Exposed - INSERT INTO users (email, "name", "password", role_type) VALUES ('takagimeow@example.com', 'takagi', 'password', 'USER')

プロセスは終了コード 0 で終了しました

TablePlusを通してデータを確認してみました。ちゃんと登録されています。

Jetpack Composeでディープリンクを開くテストをしてみる

ディープリンクを開くには次の書式でコマンドを実行する。

adb shell am start -W -a android.intent.action.VIEW -d <ディープリンクのURI> <アプリのパッケージ名>

実際に入力するときは次の様な感じ。

adb shell am start -W -a android.intent.action.VIEW -d "testapp://home_route" com.example.testapplication

アプリのパッケージ名は、build.gradleのandroid.defaultConfig.applicationIdで確認することができる。

android {
    ...
    defaultConfig {
        applicationId "com.example.testapplication"

開きたいComposableのために、routedeepLinksを定義する。testappというディープリンクスキーマを表すSCHEMEは定数としてどこかで定義しておく。

object HomeDestination : TaNavigationDestination {
    override val route = "feedl_route"
    override val destination = "feed_destination"

    val deepLinks = listOf(
        navDeepLink {
            uriPattern =
                "$SCHEME://${route}"
        }
    )
}

navigation内で呼び出されるcomposableに対して先ほど定義したroutedeepLinksを渡す。

fun NavGraphBuilder.homeGraph(
    onUpPress: () -> Unit = {},
) {
    composable(
        route = HomeDestination.route,
        deepLinks = HomeDestination.deepLinks,
    ) { navBackStackEntry ->
        HomeRoute(
            onUpPress = onUpPress,
        )
    }
}

AndroidManifest.xmlcomposableに設定した内容に沿って<intent-filter>を追加する。

<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="testapp" android:host="home_route" />
</intent-filter>
参考

アプリ コンテンツ用のディープリンクを作成する  |  Android デベロッパー  |  Android Developers

Androidの通知に表示したいアイコンを用意する

Androidでは、アプリのアイコンと通知用のアイコンは異なる。 特に異なる部分はカラーの部分で、通知用のアイコンには基本的に色をつけることはできない。

Android 5.0以降では通知機能が大幅に変更されたため、アイコンのカラーチャンネルがすべて無視される様になったのが関係しているらしい。

通知用のアイコンを考える場合、二つの要素に切り分けて考える。

1つ目は色のないアイコン。これは白色と透明な部分で構成されたsvg形式のファイルだと思って良い。アプリのロゴなどの目立たせたい部分を透明にしておくといった感じ。

もう一つは通知用のアイコンに対して動的に適用したい色の部分で、これは通知の作成時にsetColor関数に16進数の値を渡すことで設定することができる。

たとえば、次の様なアイコンを通知用に使いたい場合を考えてみる。

これをそのまま使おうとすると、Android側で色のついた部分はすべて白色に変換されてしまうため次の様に表示されてしまう。

なので、色付きだった箇所や強調したい部分を透明になるように調節した上で通知アイコンとして渡す必要がある。

ちなみに、setColor関数で色を制御できるのはあくまで通知ドロワーに表示される通知に対しての話で、ステータスバーに表示される通知アイコンの色の制御はできないので注意する必要がある。

通知を作成する際に、NotificationCompat.Builderでset系のメソッドを呼び出すタイミングでsetColor()メソッドを使って通知ドロワーに表示される際の色を指定する。

ここでは0xC0B0D7(紫)を指定している。

var builder =
        NotificationCompat.Builder(this, channelId)
            .setSmallIcon(com.example.app.R.drawable.ic_notification)
            .setColor(0xC0B0D7)
            .setContentTitle(contentTitle)
            .setContentText(contentText)
            .setPriority(NotificationCompat.PRIORITY_MAX)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)
            .build()

通知ドロワーでの表示は次の様な感じになる。

通知用のアイコンは、Android Studio上でResource Managerを開いて、プラスボタンクリックし、Import Drawablesをメニューから選択することで登録することができる。

svgファイルのインポートが完了すると、res/drawablexmlファイルが作成されるので、それをsetSmallIcon()の引数に指定する。

参考

Notifications Overview  |  Android Developers

通知の概要  |  Android デベロッパー  |  Android Developers

https://proandroiddev.com/android-iconography-notification-3acd3e00d6ec

Android Asset Studio - Notification icon generator

`FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory`がGitHub Actionsで発生した場合の対処法

問題

次のようなエラーが発生してしまい、GitHub Actions上で実行していたテストがFAILEDしてしまった。

<--- JS stacktrace --->

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
 1: 0xb02930 node::Abort() [/opt/hostedtoolcache/node/16.17.0/x64/bin/node]
 2: 0xa18149 node::FatalError(char const*, char const*) [/opt/hostedtoolcache/node/16.17.0/x64/bin/node]
 3: 0xcdd16e v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/opt/hostedtoolcache/node/16.17.0/x64/bin/node]
 4: 0xcdd4e7 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/opt/hostedtoolcache/node/16.17.0/x64/bin/node]
 5: 0xe94b55  [/opt/hostedtoolcache/node/16.17.0/x64/bin/node]
 6: 0xe95636  [/opt/hostedtoolcache/node/16.17.0/x64/bin/node]
 7: 0xea3b5e  [/opt/hostedtoolcache/node/16.17.0/x64/bin/node]
 8: 0xea45a0 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/opt/hostedtoolcache/node/16.17.0/x64/bin/node]
 9: 0xea751e v8::internal::Heap::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) 
....

結論

NODE_OPTIONS: "--max-old-space-size=5120""をenvに追加する

...
- name: Run all the tests
  run: yarn test
  working-directory: ./path/into/app
  env:
    ...
    NODE_OPTIONS: "--max-old-space-size=5120"
...

参考

ヒープ領域制限によりnode.jsのビルドが通らない場合 - Qiita

JavaScript heap out of memory が発生したときに試したこと ++ Gaji-Laboブログ