🍔
KotlinでSQLDelightを使ってデータベース操作を実装する
SQLDelightでデータベース操作を実装する
はじめに
SQLDelightは、Kotlinのマルチプラットフォーム対応のSQLフレームワークです。SQLiteデータベースに対して型安全なアクセスを提供し、KMMアプリケーションの開発をサポートします。
本記事では、実際のTodoアプリケーションのコードを例に、SQLDelightの基本的な使い方からテストの実装までを解説します。
目次
- SQLDelightの導入
- データベーススキーマの定義
- データベースのセットアップ
- CRUD操作の実装
- ViewModelとの統合
- テストの実装
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