아마 아직도 sqlite 만을 사용하는 개발자가 있을지 모르겠다.
나는 sqlite 에서 room 으로 옮겨온지가 꽤 지나서 sqlite는 이제 가물거린다.
언젠가는 room 설정도 가물거릴까 걱정하는 마음에 현제 사용하는 설정을 기록해 두려한다.
[versions]
room = "2.6.1"
[libraries]
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
plugins {
//...
alias(libs.plugins.ksp)
// Existing plugins
kotlin("plugin.serialization") version "2.0.21"
}
android{
ksp {
arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}
}
dependencies {
//room database
// https://developer.android.com/jetpack/androidx/releases/room?hl=ko
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
}
class RoomSchemaArgProvider(
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
val schemaDir: File
) : CommandLineArgumentProvider {
@Suppress("RETURN_TYPE_MISMATCH_ON_OVERRIDE") // Kotlin 의 Iterable 와 override 한 java 의 Iterable를 다른것으로 인식하는 버그 수정
override fun asArguments(): Iterable<String> {
return listOf("room.schemaLocation=${schemaDir.path}") // ksp
}
}
@TypeConverters(Converters::class)
@Database(
version = 1,
entities = [
LunaInfoEntity::class, SpcdEntity::class, ScheduleEntity::class, ShiftEntity::class,
ShiftPatternEntity::class,ShiftAddonEntity::class,
UserEntity::class,
],
exportSchema = false // Todo !!! 배포시 true 로 변경
)
abstract class EodeunCalendarMemo : RoomDatabase() {
companion object {
@Volatile // 중요 정보로 Ram 을 이용함
private var INSTANCE: EodeunCalendarMemo? = null
/**
* Get instance
* if the INSTANCE is not null, then return it,
* if it is, then create the database and save in instance variable then return it.
*
* @param context
* @return
*/
fun getInstance(context: Context): EodeunCalendarMemo {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext, EodeunCalendarMemo::class.java, "EodeunCalendarMemo.db"
).fallbackToDestructiveMigration() // todo !!! 배포시 삭제. 스키마 관리 포기 하고 강제로 진행
.build()
INSTANCE = instance
instance // return instance
} //
} // fun
} // companion object
abstract fun lunaInfoDao(): LunaInfoDao
abstract fun spcdDao(): SpcdDao
abstract fun scheduleDao(): ScheduleDao
abstract fun shiftDao(): ShiftDao
abstract fun shiftPatternDao(): ShiftPatternDao
abstract fun userDao(): UserDao
abstract fun shiftAddOnDao(): ShiftAddonDao
}
Room은 Int,String,Long 같은 프라임타입만 지원하기에 저장시 변환해서 저장해야 한다.
class Converters {
@TypeConverter
fun intToTermTime(value: Int): TermTime {
return TermTime(value)
}
@TypeConverter
fun termTimeToInt(value: TermTime): Int {
return value.toInt()
}
@TypeConverter
fun uLongToLong(value:ULong):Long = value.toLong()
@TypeConverter
fun longToULong(value:Long):ULong = value.toULong()
@TypeConverter
fun listStringToString(value: List<String>): String = Gson().toJson(value)
@TypeConverter
fun stringToShiftEntity(value: String?): ShiftEntity? {
return value?.let { Gson().fromJson(it, ShiftEntity::class.java) }
}
@TypeConverter
fun shiftEntityToString(value: ShiftEntity?): String? {
return value?.let { Gson().toJson(it) }
}
@TypeConverter
fun stringToShiftEvent(value: String?): ShiftEvent? {
return value?.let { Gson().fromJson(it, ShiftEvent::class.java) }
}
@TypeConverter
fun shiftEntityToString(value: ShiftEvent?): String? {
return value?.let { Gson().toJson(it) }
} // shiftEntityToString
@TypeConverter
fun shiftEventListToString(value: List<ShiftEvent>): String {
return Gson().toJson(value)
} // List<ShiftEvent> -> String
@TypeConverter
fun stringToShiftEventList(value: String): List<ShiftEvent> {
return Gson().fromJson(
value, object : TypeToken<List<ShiftEvent>>() {}.type
) // return Gson().fromJson(value, Array<ShiftEvent>::class.java).toList() // Array -> List 로 기능은 같은데 메모리를 약간 더 씀
} // String -> List<ShiftEvent>
@TypeConverter
fun shiftPatternToString(value: ShiftPattern?): String? {
return value?.let { Gson().toJson(it) }
} // ShiftPattern -> String
@TypeConverter
fun stringToShiftPattern(value: String?): ShiftPattern? {
return value?.let { Gson().fromJson(it, ShiftPattern::class.java) }
}
@TypeConverter
fun workHistoryItemListToString(value: List<WorkHistoryItem>): String? {
return value.let { Gson().toJson(it) }
}
@TypeConverter
fun stringToWorkHistoryItemList(value: String?): List<WorkHistoryItem>? {
return Gson().fromJson(
value, object : TypeToken<List<WorkHistoryItem>>() {}.type
)
}
}
Table 을 @Entity(tableName="String")으로 저장한다.
@Entity(tableName = "luna_info_table")
data class LunaInfoEntity(
@PrimaryKey(autoGenerate = true)
val rowId: Long = 0L, // 값이 0 이면 자동 증가
// 양력 epoch 값
val epoch: Long = 0L,
// 음력정보
val lunLeapmonth: Boolean = false, // 음력 평달 여부
val lunYear: Int = 0,
val lunMonth: Int = 0,
val lunDay: Int = 0,
val solLeapyear: Boolean = false,
// val solYear: Int = 0,
// val solMonth: Int = 0,
// val solDay: Int = 0,
val lunIljin: String = "",
val lunNday: Int = 0,
val lunSecha: String = "",
val lunWolgeon: String = "",
val solJd: Long = 0L,
// val solWeek: String = ""
) {
fun toLog(): String {
val solDate = LocalDate.ofEpochDay(epoch)
return "solYMD:${solDate.year}.${solDate.monthValue}.${solDate.dayOfMonth}.(${solDate.dayOfWeek}요일[${if (solLeapyear) "윤" else "평"}년])는 " +
"lunYMD:${lunYear}.${lunMonth}.${lunDay}.(${if (lunLeapmonth) "윤" else "평"}달) (lunIljin:${lunIljin}/lunSecha:${lunSecha}/lunWolgeon:${lunWolgeon})"
}
fun toLunaShort():String{
return "${lunMonth}.${lunDay}."
}
fun toLabel(): String {
return " ${lunYear}년 (${if (lunLeapmonth) "윤" else "평"}) ${lunMonth}월 ${lunDay}일"
}
fun toGanJi(): String {
return "${lunSecha}년 ${lunWolgeon}월 ${lunIljin}일"
}
}
DataAccessObject 는 Interface로 선언한다.
@Dao
interface LunaInfoDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(lunaInfoEntity: LunaInfoEntity): Long
@Query("SELECT EXISTS (SELECT * FROM luna_info_table WHERE epoch = :epoch) ")
suspend fun isExists(epoch:DateLong): Boolean
@Query("SELECT * FROM luna_info_table WHERE epoch = :epoch ")
fun readFlow(epoch:DateLong): Flow<LunaInfoEntity?>
@Query("SELECT * FROM luna_info_table WHERE epoch = :dateLong ")
suspend fun read(dateLong: DateLong): LunaInfoEntity?
@Query("SELECT * FROM luna_info_table WHERE epoch = :epoch ")
fun fetch(epoch:DateLong): Flow<LunaInfoEntity?>
@Query("SELECT * FROM luna_info_table ")
fun fetchAll(): Flow<List<LunaInfoEntity>>
}
DB 는 Repository 를 경유해서 접근한다.
repository 를 interface로 하여 구현체를 별도로 관리한다.
interface LunaRepo {
suspend fun isExist(dateLong: DateLong): Boolean
fun fetchFlow(dateLong: DateLong): Flow<LunaInfoEntity?>
suspend fun isExistMonth(year: Int, month: Int)
}
Repo 구현체
실제처리를 구현하고 간단한 처리를 담당해서 상단의 Viewmodel에서 처리하도록 돕니다.
class LunaRepoImpl @Inject constructor(
private val lunaDb: LunaDBRepo
) : LunaRepo {
override suspend fun isExist(dateLong: DateLong): Boolean {
return lunaDb.isExist(dateLong)
}
override fun fetchFlow(dateLong: DateLong): Flow<LunaInfoEntity?> {
return lunaDb.fetchFlow(dateLong)
}
private val _requestedLunaYearMonths = mutableListOf<Int>()
override suspend fun isExistMonth(year: Int, month: Int) {
val seed = year * 12 + month
if (_requestedLunaYearMonths.contains(seed)) return //
_requestedLunaYearMonths.add(seed)
if (lunaDb.isExist(
LocalDate.of(year, month, 1).toEpochDay()
)
) return // Api로 부터 holiday 를 가져 오도록 함
try {
DataGoKr.getLunCalInfo(year, month).response?.body?.items?.item?.forEach { it: LunaItem? ->
it?.let { lunaDb.insert(it.toEntity()) }
} //forEach
} catch (e: Exception) {
Log.e("Jim", "${e.message}")
} //try-catch
} //
}
코드주입을 위해
Hilt를 사용하게 되는데 그 주입설정이다.
@InstallIn(SingletonComponent::class)
@Module
object Injections {
// DB 와 관련된 인젝션들
@Singleton
@Provides
fun providesAppDB(@ApplicationContext context: Context): EodeunCalendarMemo {
return EodeunCalendarMemo.getInstance(context = context)
}
/* Dao 와 관련된 인젝션들 */
@Singleton
@Provides
fun provideLunaInfoDao(db: EodeunCalendarMemo): LunaInfoDao {
return db.lunaInfoDao()
}
@Singleton
@Provides
fun provideSpcdDao(db: EodeunCalendarMemo): SpcdDao {
return db.spcdDao()
}
@Singleton
@Provides
fun provideScheduleDao(db: EodeunCalendarMemo): ScheduleDao {
return db.scheduleDao()
}
@Singleton
@Provides
fun provideShiftDao(db: EodeunCalendarMemo): ShiftDao {
return db.shiftDao()
}
@Singleton
@Provides
fun provideShiftPatternDao(db: EodeunCalendarMemo): ShiftPatternDao {
return db.shiftPatternDao()
}
@Singleton
@Provides
fun provideShiftAddonDao(db: EodeunCalendarMemo): ShiftAddonDao {
return db.shiftAddOnDao()
}
@Singleton
@Provides
fun provideUserDao(db: EodeunCalendarMemo): UserDao {
return db.userDao()
}
/**
Repository 주입
*/
// It is DB repository
@Singleton
@Provides
fun provideLunaInfoRepository(dao: LunaInfoDao): LunaDBRepo {
return LunaDBRepoImpl(dao)
} // LunaDb
// It is not DB repository (over DB)
@Singleton
@Provides
fun provideLunaRepository(lunaDBRepo: LunaDBRepo): LunaRepo {
return LunaRepoImpl(lunaDBRepo)
} // Luna
// it is DB repository
@Singleton
@Provides
fun provideSpcdRepository(dao: SpcdDao): SpcdDBRepo {
return SpcdDBRepoImpl(dao)
} // SpcdDB
// It is not DB repository (over DB)
@Singleton
@Provides
fun provideHolidayRepository(spcdRepo: SpcdDBRepo): SpcdRepo {
return SpcdRepoImpl(spcdRepo)
} //Spcd
@Singleton
@Provides
fun provideShiftRepository(shiftDao: ShiftDao): ShiftRepo {
return ShiftRepoImpl(shiftDao)
}
@Singleton
@Provides
fun provideScheduleRepository(scheduleDao: ScheduleDao): ScheduleRepo {
return ScheduleRepoImpl(scheduleDao)
}
@Singleton
@Provides
fun provideShiftPatternRepository(dao: ShiftPatternDao): ShiftPatternRepo {
return ShiftPatternRepoImpl(dao)
}
@Singleton
@Provides
fun provideShiftAddonRepository(dao: ShiftAddonDao): ShiftAddonRepo {
return ShiftAddonRepoImpl(dao)
}
@Singleton
@Provides
fun provideUserRepository(dao: UserDao): UserRepo {
return UserRepoImpl(dao)
}
/**
* Viewmodel 주입
*/
@Singleton
@Provides
fun provideCoreViewmodel(
spcdRepo: SpcdRepo,
lunaRepo: LunaRepo,
userRepo: UserRepo,
scheduleRepo: ScheduleRepo
): CoreVM {
return CoreVM(spcdRepo, lunaRepo, userRepo, scheduleRepo)
}
}