Kotlin Multiplatform Project(MPP)で、Databaseを利用したい場合は現状SQLDelightというライブラリがオススメです。
2019年10月現在では、Android, iOS, JVMのサポートをしています。
SQLDelightは使い方自体はとても簡単なので、経験者の方は公式ドキュメントで細かい使い方を見ると良いと思います。
この記事はどちらかというとMPP初心者向けです。
SQLDelightとは
SQLDelight自体は2016年から存在しています。
その頃はRxJavaの全盛期であり、Reactiveなラッパーライブラリであったり、Realm
のようなNoSQLライブラリも出始め、
SQLDelightは他のDBライブラリと比較してあまり優位性を持ててはいませんでした。(主観)
しかし2018年になり、Kotlin Conf2018のセッションでJakeとAlecによってKotlin Multiplatform Projectへの対応発表を機に急激に優位性を増したように思います。(主観)
元々はhttps://github.com/square/sqldelight にあったのですが、いつの間にかSquareのサービスであるCash App
ブランドのリポジトリに移動してました。
深い意味はなさそうですが、Squareから出向したエンジニアがこっちの方が自由だからって感じで移動したのかもしれません。(推測)
ブログ
を見るとCashAppで利用するために移動していそうな感じがします。
使い方
今回は、Greetingという名前のデータベースを作成します。
カラムはシンプルにid
, hello
, bye
の3つのみです。
id | hello | bye |
---|---|---|
1 | Good afternoon | Good bye |
2 | Bonjour | Au revoir |
3 | Guten Tag | Auf Wiedersehen |
Gradle
SQLDelightはSQLの生クエリからKotlinのコード生成を行ってくれます。
初めにコード生成を行うためのプラグインをルートのbuild.gradle
に定義します。 // A
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
...
classpath 'com.squareup.sqldelight:gradle-plugin:1.2.0' // A
}
}
MPPの共通モジュールのgradleファイルで上記で定義したプラグインを読み込みます。 // B
plugins {
id 'kotlin-multiplatform'
...
id 'com.squareup.sqldelight' // B
}
// C
sqldelight {
GreetingDatabase {
packageName = "com.github.aakira.mpp.common"
sourceFolders = ["sqldelight"]
}
}
// D
kotlin {
...
sourceSets {
androidMain {
dependencies {
implementation "com.squareup.sqldelight:android-driver:1.2.0"
}
}
iosMain {
dependencies {
implementation "com.squareup.sqldelight:ios-driver:1.2.0"
}
}
jvmMain {
dependencies {
implementation "com.squareup.sqldelight:sqlite-driver:1.2.0"
}
}
}
}
Greetingという名前のデータベースを作成します。 // C
次項で説明するSQLの定義ファイルをこのパッケージに定義します。
Android, iOS, JVMそれぞれでSQLDelightのライブラリを読み込みます。 // D
SQLの定義
前項C部分で定義したパッケージ(/common/src/commonMain/sqldelight/com/github/aakira/mpp/common
)にSQLのクエリファイルGreeting.sq
を作成します。sourceFolders
がrootの階層になるので、Treeを見るとこのようになります。
.
├── android
├── ios
├── jvm
├── js
├── common
│ ├── build.gradle
│ └── src
│ ├── androidMain
│ ├── commonMain
│ │ ├── kotlin
│ │ │ └── com
│ │ │ └── github
│ │ │ └── aakira
│ │ │ └── mpp
│ │ │ └── common
│ │ │ ├── ...
│ │ │ └── Greeting.kt
│ │ └── sqldelight
│ │ └── com
│ │ └── github
│ │ └── aakira
│ │ └── mpp
│ │ └── common
│ │ └── Greeting.sq // E
│ ├── iosMain
│ ├── jsMain
│ └── jvmMain
├── build.gradle
└── settings.gradle
Greeting.sq
には普通のSQL文を定義しています。
CREATE TABLE
を最初に書いて、あとはタグ名とクエリを書きます。
CREATE TABLE Greeting(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
hello TEXT NOT NULL,
bye TEXT NOT NULL
);
insertItem:
INSERT OR REPLACE INTO Greeting(hello, bye) VALUES(?,?);
selectAll:
SELECT * FROM Greeting;
selectById:
SELECT * FROM Greeting WHERE id = ?;
SQLファイルを記述さえすれば、
generateAndroidDebugGreetingDatabaseInterface
のようなGradle Taskを実行するか、一度ビルドタスクを走らせることでSQLDelightがKotlinのファイルを生成します。
現状だと、このSQLファイルからは以下の4つが生成されます。
- Greeting - テーブルのKotlin data class
- GreetingDatabase - Databaseのinterface定義
- GreetingQueries - SQL Queryのinterface定義
- GreetingDatabaseImpl - GreetingDatabase, GreetingQueriesの実装クラス等(複数)
private class GreetingDatabaseImpl(
driver: SqlDriver
) : TransacterImpl(driver), GreetingDatabase {
override val greetingQueries: GreetingQueriesImpl = GreetingQueriesImpl(this, driver)
object Schema : SqlDriver.Schema {
override val version: Int
get() = 1
override fun create(driver: SqlDriver) {
driver.execute(null, """
|CREATE TABLE Greeting(
| id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
| hello TEXT NOT NULL,
| bye TEXT NOT NULL
|)
""".trimMargin(), 0)
}
override fun migrate(
driver: SqlDriver,
oldVersion: Int,
newVersion: Int
) {
}
}
}
このような感じで生成されていて、SQL文がハードコーディングされています。
GreetingQueriesImplにも同様にSelect文等がハードコーディングされています。
Kotlinコード
Common
SQLDelightによって自動生成されたコードを使って、実際にDBを操作していきます。
package com.github.aakira.mpp.common
import com.github.aakira.mpp.common.model.Greeting
expect fun createDb(): GreetingDatabase? // A
internal const val dbName: String = "greeting.db" // B
class GreetingDao {
private val greetingDatabase = createDb()
private val queries = greetingDatabase?.greetingQueries
// C
fun storeGreeting(hello: String, bye: String) {
queries?.insertItem(hello, bye)
}
// D
fun getGreetings(): List<Greeting> {
val queries = queries ?: return listOf()
return queries.selectAll(mapper = { _, hello, bye ->
Greeting(hello, bye)
}).executeAsList()
}
}
A. Databaseのインスタンス生成はプラットフォーム毎に異なる為、expectで定義しています
B. インスタンス生成時にDatabaseの名前を指定する必要があるのでCommonで定義しています 現状SQLDelightはJavaScriptに対応していません。 そのためJavaScriptをTargetに追加しているプロジェクトではDatabaseを生成することが出来ないためNullableになっています。
C, D. 自動生成されたコードを経由して保存と取得するメソッドを定義しています
Actual
Commonの共通コードが書けたので、Actualの実装をしていきます
- Android
lateinit var mppAppContext: Context
actual fun createDb(): GreetingDatabase? =
GreetingDatabase(AndroidSqliteDriver(GreetingDatabase.Schema, mppAppContext, dbName))
- iOS
actual fun createDb(): GreetingDatabase? = GreetingDatabase(NativeSqliteDriver(GreetingDatabase.Schema, dbName))
- JVM
actual fun createDb(): GreetingDatabase? = GreetingDatabase(JdbcSqliteDriver(IN_MEMORY))
- JavaScript
actual fun createDb(): GreetingDatabase? = null
Caller
- Android
override fun onCreate(savedInstanceState: Bundle?) {
val dao = GreetingDao()
// store
dao.storeGreeting("hello", "bye")
// get
val greetings = dao.getGreetings()
}
- iOS
override func viewDidLoad() {
let dao = GreetingDao()
// store
dao.storeGreeting(hello: "hello", bye: "bye")
// get
let greetings = dao.getGreetings()
}
AndroidもiOSもこれだけで簡単にDatabaseに値を保存することが出来ました!!
SQLDelight素晴らしい!
おまけ
SQLDelightはCoroutinesのFlow
もサポートしています。
Flowを使うと何が嬉しいのかというと、DBの値の変化を監視して変更通知を受け取ることが出来ます。
Flowを使いたい場合は、Commonモジュールのbuild.gralde
のdependenciesに追加します。
...
kotlin {
...
sourceSets {
androidMain {
dependencies {
...
implementation "com.squareup.sqldelight:coroutines-extensions:1.2.0"
}
}
}
}
...
Dao側のメソッドをFlowに変更
class GreetingDao {
fun getGreetingsFlow(): Flow<Query<Greeting>> {
val queries = queries ?: return flowOf()
return queries.selectAll(mapper = { _, hello, bye ->
Greeting(hello, bye)
}).asFlow()
}
}
- 呼び出し側
(lifecycleScope
はAndroidのlifecycle-runtime-ktx
にあるCroutineScopeです。ここでは詳しく説明はしません。)
override fun onCreate(savedInstanceState: Bundle?) {
val greetings = dao.getGreetingsFlow()
lifecycleScope.launch {
greetings.collect { query ->
query.executeAsList().forEach {
Log.v("Greeting Flow", it.hello)
}
}
}
}
先程作成したselectAllのメソッドをasFlow()
に変更してあげれば、呼び出し側でFlowを利用することが可能です。
これで、DBの中身に変更があるとgreetings.collect
が毎回発火して自動でDBの値が流れてきます。
最近はCoroutinesばかり使っているので、この感じが もはや懐かしくて良いですね(Rx lover)
Kotlin/Nativeはまだ対応していませんので、HMPP
に期待しましょう。
大したことは書いていませんが、sqldelight-example
ブランチに今回のサンプルがあります。