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つの主要ターゲットプラットフォームの実装が必要です
CMPKotlinベースのソリューションを提供しているので
androidMaincommonMainiosMain
それぞれ対応する依存関係を追加できます

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ファイルを作成します:
https://ithelp.ithome.com.tw/upload/images/20240814/201683356KGhBaS0kq.png 上記のパスに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;
クロスプラットフォームのsqlDelight実装

前述したように
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")
    }
}

重要なコードの説明

  1. AppDatabase.Schema:このSchemaは上記のBuild.gradle.ktsを設定した後
    同期すると自動生成されます。

  2. AndroidSqliteDriver:Androidプラットフォームではcontextの入力が必要なので、actual class DatabaseDriverFactoryのコンストラクタに含めます

  3. NativeSqliteDriver:iOSのSqliteDriver
Koinを使用したクロスプラットフォームDBドライバの注入

以前の記事で触れたように
クロスプラットフォームのコンテンツがある場合
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()) }
}

重要なコードの説明:

  1. platformModule:commonMainでexpectを使用して、ターゲットプラットフォームが対応する変数を実装する必要があることを知らせます
    そしてandroidMainiosMainではそれぞれactualの変数を実装する必要があります

  2. したがって、ターゲットプラットフォームandroidiOSでは
    前に完成したAppDatabaseDatabaseDriverFactoryを使用して
    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を使用したクロスプラットフォームデータベースの開発と問題解決

You might also enjoy