visible true

技術的なメモを書く

KtorでFirestore Local Emulatorにつなぐ

Firestore良さそうですよね。ローカルで動作を試せるLocal Emulatorがあるので気軽に試せてさらによさそうです。JVM環境で接続する例があんまり見当たらなかったのでメモします。

firestore local emulatorの準備をする

とりあえずインストールして、

firebase setup:emulators:firestore

次のコマンドで起動できればOKです。

firebase serve --only firestore

firebaseのバージョンとかでいろいろエラー出たりするのでググったりしてなんとか入れてください。

firestore local emulatorをポートを指定して起動する

firebase serve --only firestore ではポートが8080でしか起動できません、そこで次のコマンドでLocal Emulatorを起動します。

gcloud beta emulators firestore start --host-port=localhost:8812

次のような出力が得られれば成功です。

[firestore] API endpoint: http://localhost:8812
[firestore] If you are using a library that supports the FIRESTORE_EMULATOR_HOST environment variable, run:
[firestore]
[firestore]    export FIRESTORE_EMULATOR_HOST=localhost:8812
[firestore]
[firestore] Dev App Server is now running.
[firestore]

application.confにlocal emulatorの設定を書く

Ktorからlocalhost:8812のLocal Emulatorに接続するために、application.confに設定を書きます。 特にfirebaseのための書き方などはないので、適当に書きます。

ktor {
  firebase {
    project_id = $PROJECT_ID
    firestore {
      emulator {
        host = localhost
        port = 8812
      }
    }
  }
}

Ktorから設定を読み込む

application.confの内容はApplication.environmentから読み込めます。

fun Application.module() {
  val firestoreEmulatorHost =
    environment.config.propertyOrNull("ktor.firebase.firestore.emulator.host")?.getString()
  val firestoreEmulatorPort =
    environment.config.propertyOrNull("ktor.firebase.firestore.emulator.port")?.getString()?.toInt()
  val projectId =
    environment.config.propertyOrNull("ktor.firebase.project_id")?.getString() ?: throw IllegalStateException(
      "firebase project id not found"
    )
}

Firestoreを初期化する

FirestoreでLocal Emulatorに接続する場合はFirestoreOptionsを使って直接Firestoreのインスタンスを作ります。

val firestore = FirestoreOptions
  .newBuilder()
  .setHost("$firestoreEmulatorHost:$firestoreEmulatorPort")
  .build()
  .service

本物につなぐ場合も考慮して次のようなobjectを作りました。

import com.google.auth.oauth2.GoogleCredentials
import com.google.cloud.firestore.Firestore
import com.google.cloud.firestore.FirestoreOptions
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import com.google.firebase.cloud.FirestoreClient

object FirebaseInitializer {
  data class FirestoreEmulator(val firestoreEmulatorHost: String, val firestoreEmulatorPort: Int) {
    fun toUrl() = "$firestoreEmulatorHost:$firestoreEmulatorPort"
  }

  fun firestore(
    projectId: String,
    emulator: FirestoreEmulator?
  ): Firestore {
    return if (emulator != null) {
      FirestoreOptions
        .newBuilder()
        .setHost(emulator.toUrl())
        .build()
        .service
    } else {
      val credentials = GoogleCredentials.getApplicationDefault()
      val options = FirebaseOptions.Builder()
        .setCredentials(credentials)
        .setProjectId(projectId)
        .build()
      FirebaseApp.initializeApp(options)

      FirestoreClient.getFirestore()
    }
  }
}

こんな感じでFirestoreを取り出せます。

val firestore = FirebaseInitializer.firestore(
  projectId,
  if (firestoreEmulatorHost != null && firestoreEmulatorPort != null) {
    FirebaseInitializer.FirestoreEmulator(firestoreEmulatorHost, firestoreEmulatorPort)
  } else {
    null
  }
)

やったね。

おまけ: KoinでInjection

せっかくなのでKoinでFirestoreインスタンスをinjectできるようにします。 とりあえずKoinをdependenciesに追加し、

dependencies {
  implementation "org.koin:koin-core:2.0.1"
  implementation "org.koin:koin-ktor:2.0.1"
}

モジュールを実装します。Applicationの拡張関数にしとくと楽です。

import io.ktor.application.Application
import org.koin.dsl.module

fun Application.applicationModule() = module {
  single {
    val firestoreEmulatorHost =
      environment.config.propertyOrNull("ktor.firebase.firestore.emulator.host")?.getString()
    val firestoreEmulatorPort =
      environment.config.propertyOrNull("ktor.firebase.firestore.emulator.port")?.getString()?.toInt()
    val projectId =
      environment.config.propertyOrNull("ktor.firebase.project_id")?.getString() ?: throw IllegalStateException(
        "firebase project id not found"
      )
    FirebaseInitializer.firestore(
      projectId,
      if (firestoreEmulatorHost != null && firestoreEmulatorPort != null) {
        FirebaseInitializer.FirestoreEmulator(firestoreEmulatorHost, firestoreEmulatorPort)
      } else {
        null
      }
    )
  }
}

あとはアプリケーションの初期化時にKoinをinstallしてモジュールを設定すれば、適当に取り出せるようになります。

fun Application.module() {
  install(Koin) {
    modules(
      applicationModule()
    )
  }
  // ...
  routing {
    get("/firestore") {
      val firestore: Firestore by this@routing.inject()
        // do something
    }
  //...
}

まとめ

Firestore Local Emulator良さそう。admin的な画面って内蔵されてないんだろうか?ある気がする。わからない。