🍔

KotlinでSQLDelightを使ってデータベース操作を実装する

2024/10/24に公開

SQLDelightでデータベース操作を実装する

はじめに

SQLDelightは、Kotlinのマルチプラットフォーム対応のSQLフレームワークです。SQLiteデータベースに対して型安全なアクセスを提供し、KMMアプリケーションの開発をサポートします。

本記事では、実際のTodoアプリケーションのコードを例に、SQLDelightの基本的な使い方からテストの実装までを解説します。

目次

  1. SQLDelightの導入
  2. データベーススキーマの定義
  3. データベースのセットアップ
  4. CRUD操作の実装
  5. ViewModelとの統合
  6. テストの実装

1. SQLDelightの導入

まず、プロジェクトにSQLDelightを導入します。

// プロジェクトレベルのbuild.gradle.kts
plugins {
    id("com.squareup.sqldelight") version "1.5.5" apply false
}

// アプリレベルのbuild.gradle.kts
plugins {
    id("com.squareup.sqldelight")
}

dependencies {
    implementation("com.squareup.sqldelight:android-driver:1.5.5")
}

// SQLDelightの設定
sqldelight {
    database("AppDatabase") {
        packageName = "com.company.antodo.database"
        sourceFolders = listOf("sqldelight")
    }
}

2. データベーススキーマの定義

SQLDelightでは、純粋なSQLを使用してテーブルを定義します。以下はTodoテーブルの例です:

-- Todo.sq
CREATE TABLE Todo (
    no INTEGER NOT NULL PRIMARY KEY,
    id_user INTEGER NOT NULL,
    title TEXT NOT NULL,
    description TEXT,
    is_completed INTEGER AS Boolean DEFAULT 0 NOT NULL,
    due_date TEXT,
    priority INTEGER DEFAULT 0 NOT NULL,
    created_at TEXT,
    updated_at TEXT
);

-- クエリの定義
selectAll:
SELECT *
FROM Todo;

selectById:
SELECT *
FROM Todo
WHERE no = ? AND id_user = ?;

insertTodo:
INSERT INTO Todo(
    no,
    id_user,
    title,
    description,
    is_completed,
    due_date,
    priority,
    created_at,
    updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);

updateIsCompleted:
UPDATE Todo
SET 
    is_completed = ?,
    updated_at = ?
WHERE no = ? AND id_user = ?;

deleteById:
DELETE FROM Todo
WHERE no = ? AND id_user = ?;

3. データベースのセットアップ

Koinを使用してデータベースをDIコンテナに登録します:

val databaseModule = module {
    single<SqlDriver> {
        AndroidSqliteDriver(
            schema = AppDatabase.Schema,
            context = androidContext(),
            name = "todo.db"
        )
    }
    
    single { AppDatabase(get()) }
}

4. CRUD操作の実装

SQLDelightは定義したクエリから型安全なインターフェースを自動生成します:

class TodoRepository(private val database: AppDatabase) {
    // 全件取得
    fun getAllTodos(): Flow<List<Todo>> = 
        database.todoQueries
            .selectAll()
            .asFlow()
            .mapToList()

    // 新規追加
    suspend fun insertTodo(todo: TodoModel) {
        database.todoQueries.insertTodo(
            no = null,  // SQLiteが自動採番
            id_user = todo.idUser,
            title = todo.title,
            description = todo.description,
            is_completed = todo.isCompleted,
            due_date = todo.dueDate,
            priority = todo.priority,
            created_at = todo.createdAt,
            updated_at = todo.updatedAt
        )
    }

    // 完了状態の更新
    suspend fun updateTodoStatus(no: Long, isCompleted: Boolean) {
        database.todoQueries.updateIsCompleted(
            is_completed = isCompleted,
            updated_at = Instant.now().toString(),
            no = no,
            id_user = getCurrentUserId()
        )
    }

    // 削除
    suspend fun deleteTodo(no: Long) {
        database.todoQueries.deleteById(
            no = no,
            id_user = getCurrentUserId()
        )
    }
}

5. ViewModelとの統合

ViewModelでSQLDelightを使用する例:

class TodoViewModel(
    private val database: AppDatabase,
    private val apiService: ApiService,
    private val loginViewModel: LoginViewModel
) : ViewModel() {
    private val userId: Long
        get() = loginViewModel.userId ?: 0L

    private val _todos = MutableStateFlow<List<TodoModel>>(emptyList())
    val todos: StateFlow<List<TodoModel>> = _todos.asStateFlow()

    init {
        loadTodos()
    }

    private suspend fun loadLocalTodos() {
        try {
            val localTodos = database.todoQueries
                .selectAll()
                .executeAsList()
                .map { it.toModel() }
            _todos.value = localTodos
        } catch (e: Exception) {
            _error.value = "ローカルデータの読み込みに失敗しました: ${e.message}"
        }
    }

    fun addTodo(title: String) {
        if (title.trim().isEmpty()) return

        viewModelScope.launch(Dispatchers.IO) {
            try {
                val now = Instant.now().toString()
                val todo = TodoModel(
                    no = 0L,
                    idUser = userId,
                    title = title.trim(),
                    isCompleted = false,
                    createdAt = now,
                    updatedAt = now
                )

                // APIに保存
                val savedTodo = apiService.addTodo(todo)

                // ローカルDBに保存
                database.todoQueries.insertTodo(
                    no = savedTodo.no,
                    id_user = userId,
                    title = savedTodo.title,
                    description = savedTodo.description,
                    is_completed = savedTodo.isCompleted,
                    due_date = savedTodo.dueDate,
                    priority = savedTodo.priority,
                    created_at = savedTodo.createdAt,
                    updated_at = savedTodo.updatedAt
                )

                loadLocalTodos()
            } catch (e: Exception) {
                _error.value = "Todoの追加に失敗しました: ${e.message}"
            }
        }
    }
}

6. テストの実装

SQLDelightのテストは以下のように実装できます:

class TodoViewModelTest {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    private lateinit var database: AppDatabase
    private lateinit var todoQueries: TodoQueries
    private lateinit var viewModel: TodoViewModel

    @Before
    fun setup() {
        database = mockk(relaxed = true)
        todoQueries = mockk(relaxed = true)

        every { database.todoQueries } returns todoQueries

        val mockQuery = mockk<Query<Todo>>(relaxed = true)
        every { mockQuery.executeAsList() } returns emptyList()
        every { todoQueries.selectAll() } returns mockQuery

        viewModel = TodoViewModel(database, apiService, loginViewModel)
    }

    @Test
    fun `addTodo successfully adds todo to database`() = runTest {
        // Given
        val title = "Test Todo"
        val todo = createMockTodo(title = title)

        val mockUpdatedQuery = mockk<Query<Todo>>()
        every { mockUpdatedQuery.executeAsList() } returns listOf(todo)
        every { todoQueries.selectAll() } returns mockUpdatedQuery

        // When
        viewModel.addTodo(title)
        advanceUntilIdle()

        // Then
        coVerify {
            todoQueries.insertTodo(
                no = any(),
                id_user = 1L,
                title = title,
                description = null,
                is_completed = false,
                due_date = null,
                priority = 0L,
                created_at = any(),
                updated_at = any()
            )
        }
    }
}

まとめ

SQLDelightは以下のような利点を提供します:

  • 型安全なSQLクエリ
  • KMMサポート
  • コード生成によるボイラープレートの削減
  • SQLiteの直接的な操作
  • テストのしやすさ

実際のアプリケーション開発では、APIとの同期やキャッシュ戦略など、より複雑な要件が発生しますが、SQLDelightの柔軟性により、これらの要件にも対応することができます。

参考リンク

Discussion