마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

Room DB 초기 데이터설정의 올바른 방법

0 추천

Room DB에 초기데이터를 설정하는 것을 하고 있는데요, 결과만 말씀드리면 성공은 했습니다

근데 우연히 다른 코드를 작성하고 App Inspection 탭과 디버깅을 하면서 

특정 상황에서만 DB에 초기데이터가 설정되는 것을 확인했는데요,

그 이유가 궁금합니다.

먼저 코드입니다.

----------------

DAO

@Dao
interface WorkoutListDao {

    @Query("SELECT * FROM WorkoutList")
    fun getWorkoutList() : LiveData<List<WorkoutList>>

    @Insert
    suspend fun insertWorkoutList(workoutList: WorkoutList)
}

 

 

Database

@Database(
    entities = [WorkoutList::class],
    version = 1
)
@TypeConverters(WorkoutListTypeConverter::class)
abstract class WorkoutListDatabase : RoomDatabase() {
    abstract fun workoutListDao() : WorkoutListDao

    companion object {
        private var INSTANCE : WorkoutListDatabase? = null

        @Synchronized
        fun getDatabase(context: Context) : WorkoutListDatabase {
            // if the INSTANCE is not null, then return it,
            // if it is, then create the database
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    WorkoutListDatabase::class.java,
                    "workoutlist_db"
                )
                    .addCallback(WorkoutListCallback(context))
                    .build()
                INSTANCE = instance
                instance
            }
        }
    }
}

 

WorkoutListCallback

private const val WORKOUTLIST_JSON_FILE = "WorkoutList.json"

class WorkoutListCallback(private val context: Context) : RoomDatabase.Callback() {
    override fun onCreate(db: SupportSQLiteDatabase) {
        super.onCreate(db)
        CoroutineScope(Dispatchers.IO).launch {
            fillWithStartingWorkoutList(context)
        }
    }

    private suspend fun fillWithStartingWorkoutList(context: Context) {
        val dao = WorkoutListDatabase.getDatabase(context).workoutListDao()

        try {
            val data = loadJsonData(context) // gson으로 인해 WorkoutList의 형태로 넘어옴
            dao.insertWorkoutList(data)

        } catch (e: JSONException) {
            e.printStackTrace()
        }
    }

    private fun loadJsonData(context: Context) : WorkoutList {
        val assetManager = context.assets // assetManager 인스턴스 생성
        val inputStream = assetManager.open(WORKOUTLIST_JSON_FILE)
        
        BufferedReader(inputStream.reader()).use { reader ->// use는 사용후 열었던 스트림을 자동적으로 close 해줌
            val gson = Gson()
            return gson.fromJson(reader, WorkoutList::class.java)
        }
    }
}

 

ViewModel

class WorkoutListViewModel(application: Application) : AndroidViewModel(application){
    private val workoutDao = WorkoutListDatabase.getDatabase(application).workoutListDao()
    private val workoutListRepo = WorkoutListRepository(workoutDao)

    fun setList(part : BodyPart) {
        viewModelScope.launch(Dispatchers.IO) {
            workoutListRepo.getWorkoutList()
        }
    }
}

 

----------------------------------------------------------------------------------------------

DB에 초기데이터가 생기는 경우입니다.

 

1. insertWorkoutList(data)만 호출되었을 경우

-DB 생성도 없고 테이블 생성도 없음.

 

2.getWokroutList() 만 호출 되었을 경우

- DB 및 테이블 생성됨, 하지만 초기 데이터 저장 안됨

 

3. 둘다 호출 되었을 경우.

- 모두 정상적으로 생성되고 저장 됨.

 

저는 ViewModel에서 WorkoutListDatabase.getDatabase()가 실행되는 순간 DB가 생성되고

Callback 클래스의 insertWorkoutList()에 의해 초기 데이터가 저장되는 것을 기대했는데요,

우연히 그렇지 않다는 것을 발견하고 주석처리해가면서 확인해보니 insertWorkoutLIst()와 getWorkoutList()가 모두 호출될때

저장되는것을 확인했습니다. 왜 getWorkoutList()까지 호출되어야 저장이 되는걸까요?

 

codeslave (3,940 포인트) 님이 2022년 5월 29일 질문

1개의 답변

0 추천

WorkoutListCallback 클래스에서 CoroutineScope을 생성하셨다면 해제를 해주셔야 할 것 같은데요. 메모리누수가 우려됩니다.

fillWithStsrtingWorkoutList에는 context애서 db를 참조하는 대신 onCreate에 린자로 넘어 온db나 필효한
dao를 넘겨주셔도 될 것 같네요.

그리고 데이터를 insert했으나 아무 데이터도 보이지 않는 것은, transaction이 시작은 됐지만, 완료가 되지않는 것 같네 들리네요.
insert후에 select를 바로 실행해서 된다면, transaction 문제로 보입니다. 이게 맞다면, 초기데이터를 집어넣을 때 transaction 처리를 한번 해보실래요?
 

@Dao
interface WorkoutListDao {
 
    ...
 
    @Transaction //<---
    @Insert
    suspend fun insertWorkoutList(workoutList: WorkoutList)
}



https://developer.android.com/reference/androidx/room/Transaction

spark (227,510 포인트) 님이 2022년 5월 29일 답변
spark님이 2022년 5월 29일 수정
선생님 Room 을사용할때 혹시 간혹 잘되던 코드가 DB에 저장안된다던지 하는 버그같은것이 있나요? 이틀전에 저장되는것까지 다 보고 오늘 선생님 답변보고 해보려고하는데 기존 코드가 저장이 안되네요 전혀.. 코드는 건드린게 없는데 말이죠..ㅠ
Clean Project/ReBuild Project 등등 해봤는데 이상하게 저장이 안되네요..
Database 클래스에서
instance.getOpenHelper().getWritableDatabase() 이 DB를 강제로 여는 코드로하면 저장이 되던데 기존 코드가안되니까 답답하네요
Room 테스트코드를 작성해 보세요. 코드에 변경사항이 자주 생길수록 테스트코드가 있어야 변경을 자신감있게 할 수 있고 테스트의 난이도도 줄어들기 때문에, 테스트를 작성해야 한다는 것이 제가 요즘 깨닫는 점입니다. 테스트는 버그를 잡는데도 도움을 주지만, 개발속도를 높여주는데 아주 중요한 툴이라고 생각합니다.

그리고 DB에 저장이 안되는 부분은 앱을 삭제했다 다시 설치해서 테스트 해보세요. 저장시 에러가 없는 건지 저장하는 부분을 스킵한다던지 하지는 않는지 디버깅을 해보시구요. 님과 같은 증상이 있다는건 들어보지 못했구요. 님의 코드와 관련이 높아 보입니다.
추가적으로 insert, update, delete를 수행하는 Dao의 함수에는 @Transaction을 사용하는 것이 합리적으로 보입니다.
선생님 Dao 클래스가 문제였네요.. 제가 뭘하다가 건드린건지는 모르겠는데..
getWorkoutList가 suspend 함수가 아니어서 그랬네요
이 함수를 Viewmodel의 코루틴에서 호출하는데요. getWorkoutList()를 suspend로 만들어주니 데이터가 정삭적으로 들어갔습니다.
insertWorkoutList는 suspend 이던말던 상관없는거같구요
getWorkoutList()가 suspend가 아니었던것이 왜 원인이 되는건지 잘모르겠습니다. 왜 getWorkoutList()가 suspend 함수여야 동작하는지 잘이해가 가지 않습니다. 코루틴안에서 일반함수도 동작하지 않나요?


그리고 코드랩같은 개발자 샘플 코드같은곳이나 다른 사람들의 ㅇ샘플코드를 보면 전부 DB에서 데이터를 get하는 함수는 suspend로 두지 않던데요..insert할때만 suspend를 사용하더라구요

어떤 이유때문인가요? 왜 getWorkoutList()이 suspend일때만 동작하는지..
그리고 getWorkoutList() 같이 데이터를 가져오는 쿼리를 사용할때는
코루틴을 사용하면 안되는걸까요?
이유를 알 것 같네요. 해당 함수의 리턴값이 LiveData라서 그런 것으로 보입니다. ViewModel의 extension중에  liveData함수가 있는데 이 함수도 동일하게 LiveData를 리턴합니다. 함수 내부를 보면 corountine scope만들어지는데 여기에 사용되는 body가 님이 선언하신 함수로 보면  될 것 같구요. Suspend 함수를 사용하지 않으면 코틀린 컴파일러가 콜백을 제대로 처리할 수 없어서 문제가 되는 걸로 보입니다.ViewModel에서는 suspend함수만 쓰도록 경고가 나오는데 Room자체적인 어노테이션 프로세스거 더 있어서 그 부분까지는 아직 지원하지 못하는 것처럼 보이네요.
감사합니다. LiveData나 Flow 둘중하나 써야할것같은데 이것이 문제였네요..
어떻게 생겨먹었나 보려고 Viewmodel extension이라는것을 찾아보려고했는데 못찾아서..(+수정 생각해보니 getWorkoutList의 값은 LiveData가 아니어도 될것같습니다..)

그리고 DAO 클래스의 insert에 Transaction 어노테이션을 붙이고 getWorkoutList()없이 실행해봤는데 결과는 여전히 똑같았어요. DB 테이블 둘다 생성이 안되더라구요. 왜인지 데이터를 불러오는 getWorkoutLIst() 꼭 있어야
DB 및 테이블이 생성되고 데이터가 들어가더라구요..

암튼 https://stackoverflow.com/questions/72400062/room-database-insert-static-data-before-other-crud-operations/72400960#72400960

여기 답변한사람은 DAO 클래스를 이용하는 것을 추천하지 않더군요.
Callback 함수의 override 된 onCreate()를 보시면 매개변수로 SupportSQLiteDatabase가 전달되는데 이걸 이용해서 db에 insert하는 것을 추천하더라구요.
db.execSQL 을 이용하던지, ContentValue를 이용해서 put()한후에 db.insert()하는 방식으로요. 딱봐도 각 열마다 값을 넣어주고 설정해줘야해서 더 번거로운 방식인데 DAO를 사용해도 문제될건 없죠?

저는 getWorkoutLIst() 될때만 DB가 생성되고 데이터가 저장되는것이 찝찝하긴한데 instance.getOpenHelper().getWritableDatabase() 이걸로 강제로 DB를 열어서 하면 getWokroutLIst()가 필요없긴하더라구요 이것도 찝찝하긴 매한가지지만요
흥미로운 글이네요. 네 Dao에 직접 @Transaction어노테이션을 다는 건 호불호가 있는 것 같구요. Transaction이 필요한 곳은
https://developer.android.com/reference/androidx/room/RoomDatabase#runInTransaction(java.lang.Runnable)
runInTransaction을 사용하는 것이 좀 더 나은 옵션처럼 보이네요. 님처럼 대용량 데이터를  넣는 작업은 transaction 처리를 하는 것이 안정적으로 보이구요.
그리고 Dao자체가 문제는 아닌 것 같구요. 굳이 옛날방식으로 SQLite를 직접 다룰 필요는 없어 보여요.
한가지 궁금한게
loadJsonData함수를 보면 WorkoutList를 한개만 로드하는데, db에 데이터를 한개만 저장하는 건가요?
List<WorkoutList>를 저장하지는 않는데, getWorkoutList에서는 List를 읽어오는게 좀 이상해서요.
insert도 List<WorkoutList>를 하는게 맞을 듯 해 보이는데요...
감사합니다. 그냥 기존에 하던대로 해야할것 같긴한데 이래저래 에러사항이 많네요..본문의 코드에는 안나와있지만 viewmodel에서 when 절로 part에 따른 리스트를 불러오는게 있는데 이걸 repository에 옮긴후 DB에서 가져오려하니 여기선 또 DB가 생성이 안되어 Null object 에러가 생긴다거나 그러네요..꽤나 머리가 아프네요ㅠ

무튼
getWorkoutList에서 LiveData<List<WokroutList>>라고 WorkoutLIst를 불러오는건 잘못된 코드입니다ㅠ Wokrout:LIst하나만 저장하고 하나만 불러오는 형식으로 할예정입니다.
getWorkoutList에서 LiveData 형식으로 로드를 하려고했는데..그렇게 할필요가 없다고 판단해서 WorkoutList 만 불러오고 있습니다..
...