Compose Multiplatform 実践:CMPでSqlDelightを使用したクロスプラットフォームデータベースの実装
はじめに
Compose Multiplatform (略称CMP)
CMPプロジェクトでは
クロスプラットフォームのデータベース操作をどのように実現するのでしょうか?
SqlDelightはそのための強力なソリューションを提供しています
本記事ではクロスプラットフォームAndroidおよびiOS環境で
SqlDelightを使用してデータベース操作を行う方法を紹介します
CMPにSqlDelightを実装する
プロジェクトにSqlDelightを導入するには
libs.version.tomlファイルに以下を追加します:
[versions]
sqldelight = "2.0.1"
[libraries]
sqldelight-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" }
sqldelight-coroutines-extensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
/** extensions と runtime はどちらか一方を選択可能 * /
/** 主な違いは、extensionsがよく使われるflow、emit関連の操作を提供するのに対し、runtimeは提供しない点 * /
sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" }
[plugins]
sqlDelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
そしてbuild.gradle.ktsにプラグインと依存関係を追加します
今回は2つの主要ターゲットプラットフォームの実装が必要です
CMPはKotlinベースのソリューションを提供しているので
androidMain、commonMain、iosMainに
それぞれ対応する依存関係を追加できます
androidMain.dependencies {
implementation(libs.sqldelight.android)
}
commonMain.dependencies {
implementation(libs.sqldelight.coroutines.extensions)
}
iosMain.dependencies {
implementation(libs.sqldelight.native)
}
同様に、build.gradle.ktsの
kotlinセクションの下にsqlDelightの設定を追加します
このgradle設定は
test.your.package.dbパッケージ内で
コンパイル後にAppDatabaseという操作可能なクラスが生成されると理解できます
kotlin {
sqldelight {
databases {
create("AppDatabase") {
packageName.set("test.your.package.db")
}
}
}
}
commonMain/sqldelight/databaseディレクトリに.sqファイルを作成します:
上記のパスにsqldelightフォルダを追加する必要があります
そうすることでプロジェクトをビルドする際に操作可能なクラスが正常に生成されます
sqlDelightの.sqファイルは
SQLコマンドを通じて
追加・削除・更新・検索を定義してテーブルを生成するソリューションを提供します
.sqの例は以下の通りです:
CREATE TABLE VocabularyEntity (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
insert:
INSERT OR REPLACE INTO VocabularyEntity(id, name)
VALUES(?,?);
getAll:
SELECT * FROM VocabularyEntity;
updateName:
UPDATE VocabularyEntity
SET name = :name
WHERE id IS :id;
delete:
DELETE FROM VocabularyEntity
WHERE id IS :id;
前述したように
AndroidとiOSプラットフォームの互換性の違いにより
テーブル関連の操作ロジックは共通化できますが
異なるターゲットのDatabaseDriverは個別に実装する必要があるかもしれません
したがって
異なるプラットフォーム用のDatabaseDriverを作成するには
次のように実装できます:
// in ../commonMain
expect class DatabaseDriverFactory {
fun create(): SqlDriver
}
// in ../androidMain
actual class DatabaseDriverFactory(private val context: Context) : KoinComponent {
actual fun create(): SqlDriver {
return AndroidSqliteDriver(AppDatabase.Schema, context, "AppDataBase")
}
}
// in ../iosMain
actual class DatabaseDriverFactory {
actual fun create(): SqlDriver {
return NativeSqliteDriver(AppDatabase.Schema, "AppDataBase")
}
}
重要なコードの説明:
-
AppDatabase.Schema:このSchemaは上記のBuild.gradle.ktsを設定した後同期すると自動生成されます。
-
AndroidSqliteDriver:Androidプラットフォームではcontextの入力が必要なので、actual classDatabaseDriverFactoryのコンストラクタに含めます -
NativeSqliteDriver:iOSのSqliteDriver
以前の記事で触れたように
クロスプラットフォームのコンテンツがある場合
platformModuleを作成して、クロスプラットフォームで注入する必要があるコンテンツを実装できます
(忘れた場合はCMPでKoinを使用した依存性注入を参照してください)
ここでは例を直接示します:
// in ../commonMain
expect val platformModule: Module
// in ../androidMain
actual val platformModule: Module = module {
single { DatabaseDriverFactory(get<Context>()) }
single { AppDatabase(get<DatabaseDriverFactory>().create()) }
}
// in ../iosMain
actual val platformModule: Module = module {
single { DatabaseDriverFactory() }
single { AppDatabase(get<DatabaseDriverFactory>().create()) }
}
重要なコードの説明:
-
platformModule:commonMainでexpectを使用して、ターゲットプラットフォームが対応する変数を実装する必要があることを知らせますそして
androidMain、iosMainではそれぞれactualの変数を実装する必要があります -
したがって、ターゲットプラットフォーム
androidとiOSでは前に完成した
AppDatabaseとDatabaseDriverFactoryを使用してkoinで依存性注入を行うことができます
以下はAppDatabaseを取得して操作を行う例です
SqlDelightはオブジェクト指向の操作可能なメソッドに変換して使用できるようにします
class LearningDataStore (private val db: AppDatabase) {
private val queries = db.vocabularyEntityQueries
fun insert(id: Long?, name: String) {
queries.insert(id = id, name = name)
}
fun getAll() = queries.getAll().asFlow().mapToList(Dispatchers.IO)
fun update(id: Long, name: String) {
queries.updateName(id = id, name = name)
}
fun delete(id: Long) {
queries.delete(id = id)
}
}
まとめ
これは初期のCMPが提供するローカルデータベースのソリューションの一つです
クロスプラットフォームのDB論理を共有するために使用されます
しかし現在の使用感ではあまり直感的ではありません
SQLコマンド全体を自分で書く必要があるためです
しかし一度に両プラットフォームのロジックを書くことができれば
これも良い方法と言えるでしょう
しかし心配する必要はありません
後でRoomを使ったクロスプラットフォームローカルDBの新しいソリューションを紹介します
これは最近の2024/05頃にようやくサポートされ始めたものです
ですので、その時にもぜひ参考にしてみてください
次の記事では
別のローカルデータベースRoomについて紹介します
参考にした後
どちらを使用するかを決めることができます
Compose Multiplatform 実践:CMPでROOMを使用したクロスプラットフォームデータベースの開発と問題解決