본문 바로가기

Android/Architecutre

MVVM 시작하기(2) ― Room Entity, Dao, Database 만들기

728x90
반응형

 

 

 

앞의 글을 참조해 주세요.

[Android/이론] - MVVM 시작하기(1) ― LiveData, Room을 MVVM패턴으로 사용해보자

 

먼저, 데이터를 저장할 Room의 Entity, Dao, Database를 만들어 주겠습니다.

 

Room


 

  • 종속성 추가
apply plugin: 'kotlin-kapt'

...

dependencies{
  ...
  // Room components
  def roomVersion = '2.2.5'
  implementation "androidx.room:room-runtime:$roomVersion"
  kapt "androidx.room:room-compiler:$roomVersion"
  implementation "androidx.room:room-ktx:$roomVersion"
  androidTestImplementation "androidx.room:room-testing:$roomVersion"
  
  // Kotlin components
  def coroutines = '1.3.4'
  def kotlin_version = "1.3.72"
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
  api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines"
  api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
  
}

 

Room의 종속성 추가와 함께, Coroutine 사용을 위해 Coroutine 종속성도 추가해주었습니다.

 

 

  • UserEntity.kt
@Entity(tableName = "user_table")
data class UserEntity(
    @PrimaryKey
    @ColumnInfo(name="name")
    val name: String,

    @ColumnInfo(name="gender")
    val gender: String?,

    @ColumnInfo(name="birth")
    val birth: String?
)

 

PrimaryKey를 자동적으로 생성해주게 하려면 다음처럼 사용할 수 있습니다.

@PrimaryKey(autoGenerate = true) 
val id: Int

 

PrimaryKey는 Table 내의 식별자라고 생각하시면 됩니다.

 

  • UserDao.kt
@Dao
interface UserDao {

    @Query("SELECT * from user_table ORDER BY name ASC")
    fun getAlphabetizedUsers(): LiveData<List<UserEntity>>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(userEntity: UserEntity)

    @Query("DELETE FROM user_table")
    suspend fun deleteAll()
}

 

  • getAlphabetizedUsers()를 통해 "user_table"내의 "name" 컬럼 기준으로 모든 UserEntity를 오름차순(ASC) 정렬하여 가져옵니다. (*DESC: 내림차순)
  • return 값 List<UserEntity>를 LiveData로 감싸주어 변화를 감지합니다.
  • insert 메서드를 통해 UserEntity에 데이터를 삽입할 수 있습니다.
  • onConflict = OnConflictStrategy.IGNORE 의 뜻은 같은 값이 들어왔을 때 무시한다는 뜻입니다.
Constants
ABORT OnConflict strategy constant to abort the transaction.
FAIL OnConflict strategy constant to fail the transaction.
IGNORE OnConflict strategy constant to ignore the conflict.
REPLACE OnConflict strategy constant to replace the old data and
continue the transaction.
ROLLBACK OnConflict strategy constant to rollback the transaction
  • insert, deletAll 메서드는 데이터베이스의 추가/삭제 이므로, 코루틴 스코프(또는 스레드)에서 사용해주어야 합니다. 코루틴 스코프에서 사용하기 위해 suspend를 붙여주었습니다.

> 다른 annotation 확인하기

 

  • 참고
  • update, delete
@Update
fun update(user: UserEntity);

@Delete
fun delete(user: UserEntity);

 

  • id로 데이터 가져오기
@Query("select * from tool_table where id = :toolId")
suspend fun loadTool(toolId: Int): ToolEntity

 

  • 특정 필드 업데이트
@Query("UPDATE user_table SET name = :name WHERE id = :id")
suspend fun updateName(id: Int, name: String)

 

  • 특정 단어가 포함된 데이터 가져오기
@Query("SELECT * FROM hamster WHERE name LIKE :search")
fun loadHamsters(search: String?): Flowable<List<Hamster>>

 

  • 쿼리 검색
@Query("SELECT tool_table.* FROM tool_table JOIN toolsFts ON (tool_table.name = toolsFts.name) WHERE toolsFts MATCH :query")
fun searchAllTools(query: String?): LiveData<List<ToolEntity>>

[Android/Function] - [안드로이드] Room 데이터베이스의 검색기능 구현하기 ― Room Fts4

 

  • AppDatabase.kt
@Database(entities = [UserEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null
        fun getDatabase(
            context: Context,
            scope: CoroutineScope
        ): AppDatabase {
            // 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,
                    AppDatabase::class.java,
                    "app_database"
                )
                    .addCallback(AppDatabaseCallback(scope))
                    .fallbackToDestructiveMigration()
                    .build()
                INSTANCE = instance
                // return instance
                instance
            }
        }
    }

    private class AppDatabaseCallback(
        private val scope: CoroutineScope
    ) : RoomDatabase.Callback() {

        override fun onCreate(db: SupportSQLiteDatabase) {
            super.onCreate(db)
            INSTANCE?.let { database ->
                scope.launch {
                    populateDatabase(database.userDao())
                }
            }
        }
        suspend fun populateDatabase(userDao: UserDao) {
            
            //userDao.deleteAll()
            // Add User
            userDao.insert(UserEntity("Lilly","여","1993-07-25"))
        }
    }

}

 

  • Database class는 추상클래스, Singletone으로 구현되어야 합니다.
  • 일반적으로 Database는 전체 앱에 하나의 인스턴스만 만듭니다.
  • .addCallback(AppDatabaseCallback(scope))

RoomDatabaseCallback을 만들어 onCreate()을 override 하여 데이터베이스가 처음 생성되었을때 할 행동을 코딩할 수 있습니다.

onOpen()를 override하면 데이터베이스가 열릴때마다 할 활동을 만들 수 있습니다.

onCreate에서 데이터베이스에 처음 데이터를 넣어주고, 이를 addCallback()을 통해 추가해주었습니다.

insert는 데이터의 추가이므로 coroutine을 사용하였습니다.

  • .fallbackToDestructiveMigration()

본래 database의 schema가 바뀌었을 경우 version을 변경해 주어야 합니다.

@Database(entities = [UserEntity::class], version = 1, exportSchema = false)

 

위의 annotation을 확인해주세요.

dao나 entity등이 변화했을 경우 version을 2로 바꾸어주고, migration을 진행해야 합니다.

따로 이전 데이터의 저장 없이 즉, migration 구현 없이 전의 데이터를 지우고 새로운 버전을 시작하기 위해 fallbackToDestructiveMigration()을 추가하였습니다.

> migration 구현 참조하기

  • exportSchema = false

true로 하면 Room의 schema를 json파일로 생성할 수 있습니다.

> Schema Export

  • entities = [UserEntity::class]

entity가 여러개일 경우, array에 entity를 더 추가해 주면 됩니다.

 

.getDatabaseCreated를 이용하여 생성된 데이터베이스를 가져오는 메서드도 추가 할 수 있습니다.

    private val mIsDatabaseCreated = MutableLiveData<Boolean>()
    ...
    
    /**
     * Check whether the database already exists and expose it via [.getDatabaseCreated]
     */
    private fun updateDatabaseCreated(context: Context) {
        if (context.getDatabasePath(DATABASE_NAME).exists()) {
            setDatabaseCreated()
        }
    }

    private fun setDatabaseCreated() {
        mIsDatabaseCreated.postValue(true)
    }

    open fun getDatabaseCreated(): LiveData<Boolean> {
        return mIsDatabaseCreated
    }

 

getDatabase()에서 인스턴스가 생셩될 때 아래 코드를 추가해주면 됩니다.

INSTANCE?.updateDatabaseCreated(context.applicationContext)

 

  • 최종 예제 코드 참조
@Database(
    entities = [UserEntity::class, ToolEntity::class, ToolFtsEntity::class],
    version = 8,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao
    abstract fun toolDao(): ToolDao

    private val mIsDatabaseCreated = MutableLiveData<Boolean>()

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null
        private const val DATABASE_NAME = "app_database"
        fun getDatabase(
            context: Context,
            scope: CoroutineScope
        ): AppDatabase {
            // 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,
                    AppDatabase::class.java,
                    DATABASE_NAME
                )
                    .addCallback(AppDatabaseCallback(scope))
                    .fallbackToDestructiveMigration()
                    .build()
                INSTANCE = instance
                INSTANCE?.updateDatabaseCreated(context.applicationContext)
                // return instance
                instance
            }
        }
    }

    private class AppDatabaseCallback(
        private val scope: CoroutineScope
    ) : RoomDatabase.Callback() {

        override fun onOpen(db: SupportSQLiteDatabase) {
            super.onOpen(db)
            INSTANCE?.let { database ->
                scope.launch {
                    populateDatabase(database.toolDao())
                }
            }
        }
        suspend fun populateDatabase(toolDao: ToolDao) {
            toolDao.deleteAll()
            val products: List<ToolEntity> = DataGenerator.generateTools()
            for(p in products){
                toolDao.insert(p)
            }

        }
    }

    /**
     * Check whether the database already exists and expose it via [.getDatabaseCreated]
     */
    private fun updateDatabaseCreated(context: Context) {
        if (context.getDatabasePath(DATABASE_NAME).exists()) {
            setDatabaseCreated()
        }
    }

    private fun setDatabaseCreated() {
        mIsDatabaseCreated.postValue(true)
    }

    open fun getDatabaseCreated(): LiveData<Boolean> {
        return mIsDatabaseCreated
    }


}

 

이어서, Room을 ViewModel, Repository, View와 연결해 보겠습니다.

[Android/이론] - MVVM 시작하기(3) ― ViewModel, 데이터 바인딩(Data Binding)

 

 

 

 

 

 

 

 

728x90
반응형