KC Blog

Compose Multiplatform 実践:CMPでSqlDelightを使用したクロスプラットフォームデータベースの実装

7 min read
CrossPlatform#CMP#Kotlin

はじめに

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を使用したクロスプラットフォームデータベースの開発と問題解決