Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Room 데이터베이스, 마이그레이션 세팅 및 GoalDao 구현 #17

Merged
merged 21 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions core/database/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
plugins {
alias(libs.plugins.dobedobe.android.library)
alias(libs.plugins.room)
alias(libs.plugins.ksp)
}

android {
namespace = "com.chipichipi.dobedobe.core.database"

sourceSets {
getByName("androidTest").assets.srcDir("$projectDir/schemas")
}
}

ksp {
arg("room.generateKotlin", "true")
}

room {
schemaDirectory("$projectDir/schemas")
}

dependencies {
api(projects.core.model)
implementation(platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)

androidTestImplementation(libs.androidx.room.testing)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "8d37e040dbba463eff631f177a3d3b67",
"entities": [
{
"tableName": "Goal",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `isPinned` INTEGER NOT NULL, `isCompleted` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `completedAt` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isPinned",
"columnName": "isPinned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isCompleted",
"columnName": "isCompleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "completedAt",
"columnName": "completedAt",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8d37e040dbba463eff631f177a3d3b67')"
]
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.chipichipi.dobedobe.core.database.dao

import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import com.chipichipi.dobedobe.core.database.db.DobeDobeDatabase
import com.chipichipi.dobedobe.core.database.entity.GoalEntity
import com.chipichipi.dobedobe.core.database.fixtures.fakeGoalEntities
import com.chipichipi.dobedobe.core.database.fixtures.fakeGoalEntity
import io.kotest.matchers.booleans.shouldBeTrue
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.Before
import kotlin.test.Test

class GoalDaoTest {
private lateinit var goalDao: GoalDao

@Before
fun setUp() {
goalDao =
Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
DobeDobeDatabase::class.java,
).build().goalDao()
}

@Test
fun 목표_저장_테스트() =
runTest {
// given
val goals: List<GoalEntity> = fakeGoalEntities("준원", "준혁")

// when
goalDao.insertGoals(goals)

// then
val retrievedGoals: List<GoalEntity> = goalDao.getGoals().first()
val retrievedGoalTitles = retrievedGoals.map { it.title }
retrievedGoals.shouldHaveSize(2)
retrievedGoalTitles shouldBe listOf("준원", "준혁")
}

@Test
fun 목표_저장후_삭제_테스트() =
runTest {
// given
val goal: GoalEntity = fakeGoalEntity(id = 1L, title = "준원")

// when
goalDao.insertGoal(goal)
goalDao.deleteGoal(goalId = 1L)
// then
val retrievedGoals: List<GoalEntity> = goalDao.getGoals().first()
retrievedGoals.shouldHaveSize(0)
}

@Test
fun 목표_완료하기_테스트() =
runTest {
// given
val goal: GoalEntity = fakeGoalEntity(id = 1L, title = "준원", isCompleted = false)
// when
goalDao.insertGoal(goal)
goalDao.updateGoal(goal.copy(isCompleted = true, completedAt = Instant.DISTANT_FUTURE))
// then
val retrievedGoal: GoalEntity = goalDao.getGoal(1L).first()
retrievedGoal.isCompleted.shouldBeTrue()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.chipichipi.dobedobe.core.database.db

import android.content.Context
import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
import org.junit.jupiter.api.DisplayName
import java.io.IOException

class DobeDobeMigrationTest {
@get:Rule
val helper =
MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
DobeDobeDatabase::class.java,
)

@Test
@DisplayName("모든 database 버전 마이그레이션 테스트")
@Throws(IOException::class)
fun test_migrateAll() {
// given
val dbName = "TEST_DB"
val oldestDbVersion = 1
val testContext = ApplicationProvider.getApplicationContext<Context>()

// when : 가장 오래된 버전의 DB를 생성하고 닫음
helper.createDatabase(dbName, oldestDbVersion)
.close()

// then : 최신 버전 DB로 마이그레이션 후 검증
Room.databaseBuilder(
testContext,
DobeDobeDatabase::class.java,
dbName,
).addMigrations(*DobeDobeDatabase.MIGRATIONS)
.build().apply {
openHelper.writableDatabase.close()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.chipichipi.dobedobe.core.database.fixtures

import com.chipichipi.dobedobe.core.database.entity.GoalEntity
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

fun fakeGoalEntities(
vararg titles: String,
): List<GoalEntity> {
return titles.mapIndexed { index, title ->
fakeGoalEntity(
id = (index + 1).toLong(),
title = title,
)
}
}

fun fakeGoalEntity(
id: Long,
title: String,
isPinned: Boolean = false,
isCompleted: Boolean = false,
createdAt: Instant = Clock.System.now(),
completedAt: Instant? = null,
): GoalEntity {
return GoalEntity(
id = id,
title = title,
isPinned = isPinned,
isCompleted = isCompleted,
createdAt = createdAt,
completedAt = completedAt,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.chipichipi.dobedobe.core.database.convertor

import androidx.room.TypeConverter
import kotlinx.datetime.Instant

internal class InstantConverter {
@TypeConverter
fun longToInstant(value: Long?): Instant? =
value?.let(Instant::fromEpochMilliseconds)

@TypeConverter
fun instantToLong(instant: Instant?): Long? =
instant?.toEpochMilliseconds()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.chipichipi.dobedobe.core.database.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.chipichipi.dobedobe.core.database.entity.GoalEntity
import kotlinx.coroutines.flow.Flow

@Dao
interface GoalDao {
@Query("SELECT * FROM Goal")
fun getGoals(): Flow<List<GoalEntity>>

@Query("SELECT * FROM Goal WHERE id = :id")
fun getGoal(id: Long): Flow<GoalEntity>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertGoal(goals: GoalEntity)

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertGoals(goals: List<GoalEntity>)

@Query("DELETE FROM Goal WHERE id = :goalId")
suspend fun deleteGoal(goalId: Long)

@Update
suspend fun updateGoal(goal: GoalEntity)

@Query("DELETE FROM Goal")
suspend fun clear()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.chipichipi.dobedobe.core.database.db

import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import com.chipichipi.dobedobe.core.database.convertor.InstantConverter
import com.chipichipi.dobedobe.core.database.dao.GoalDao
import com.chipichipi.dobedobe.core.database.entity.GoalEntity

@Database(
entities = [GoalEntity::class],
version = 1,
)
@TypeConverters(
InstantConverter::class,
)
internal abstract class DobeDobeDatabase : RoomDatabase() {
abstract fun goalDao(): GoalDao

companion object {
const val DATABASE_NAME = "dobedobe.db"
val MIGRATIONS: Array<Migration> = arrayOf()
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.chipichipi.dobedobe.core.database.di

import com.chipichipi.dobedobe.core.database.dao.GoalDao
import com.chipichipi.dobedobe.core.database.db.DobeDobeDatabase
import org.koin.dsl.module

val daoModule =
module {
includes(databaseModule)

single<GoalDao> { get<DobeDobeDatabase>().goalDao() }
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
package com.chipichipi.dobedobe.core.database.di

import androidx.room.Room
import com.chipichipi.dobedobe.core.database.db.DobeDobeDatabase
import org.koin.dsl.module

internal val databaseModule =
module {
single<DobeDobeDatabase> {
Room.databaseBuilder(
get(),
DobeDobeDatabase::class.java,
DobeDobeDatabase.DATABASE_NAME,
).fallbackToDestructiveMigration() // TODO: 최초 배포 시 삭제
.addMigrations(*DobeDobeDatabase.MIGRATIONS)
.build()
}
}
Loading
Loading