Kotlinでプロジェクト内のディレクトリにある複数のJSONファイルをまとめて処理してみる
次の様なディレクトリの構成を考えてみる。
- a_lot_of_settings
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}") } } }
参考
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とPostgreSQLのJDBCドライバを使って接続を行う
gradle.properties
にて、exposed_version
とpostgresql_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 APIでusers
テーブルを定義する。
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
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.dao
にDatabaseFactory
を実装してみます。
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() ... }
そしてUsers
はDSLで定義します。
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のために、route
とdeepLinks
を定義する。testapp
というディープリンクのスキーマを表すSCHEME
は定数としてどこかで定義しておく。
object HomeDestination : TaNavigationDestination { override val route = "feedl_route" override val destination = "feed_destination" val deepLinks = listOf( navDeepLink { uriPattern = "$SCHEME://${route}" } ) }
navigation
内で呼び出されるcomposable
に対して先ほど定義したroute
とdeepLinks
を渡す。
fun NavGraphBuilder.homeGraph( onUpPress: () -> Unit = {}, ) { composable( route = HomeDestination.route, deepLinks = HomeDestination.deepLinks, ) { navBackStackEntry -> HomeRoute( onUpPress = onUpPress, ) } }
AndroidManifest.xmlにcomposable
に設定した内容に沿って<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/drawable
にxmlファイルが作成されるので、それをsetSmallIcon()
の引数に指定する。
参考
Notifications Overview | Android Developers
通知の概要 | Android デベロッパー | Android Developers
https://proandroiddev.com/android-iconography-notification-3acd3e00d6ec
`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" ...