Skip to content

Commit

Permalink
Room 데이터베이스, 마이그레이션 세팅 및 GoalDao 구현 (#17)
Browse files Browse the repository at this point in the history
* feat: add Goal property createdAt, compledAt

* build: add room dependency

* feat: add GoalEntity

* feat: add InstantConvertor

* feat: add db, dao

* feat: load to koinModule

* chore: delete .gitkeep

* test: add goalEntity fixtures

* test: GoalDao Test

* feat: db migration base

* test: db migration All test

* style: ktFormat

* chore: rename dao insert Method

* chore: DobeDobeDatabase add internal modifier

* chore: rearrange db annotation

* fix:  change to generic type DobeDobeDatabase

* chore: import Goal.State

* style: ktFormat

* refactor: migrate Goal.State to isCompleted

* chore: reformat
  • Loading branch information
murjune authored Jan 24, 2025
1 parent 4ba89ac commit 6de7c5e
Show file tree
Hide file tree
Showing 19 changed files with 441 additions and 54 deletions.
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,
)
}
Empty file.
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

0 comments on commit 6de7c5e

Please sign in to comment.