본문 바로가기

안드로이드

[리팩토링] 3. SQLite에서 Room으로 리팩토링

개요

HEALTH-ER 앱은 모든 DB에 관한 코드를 SQLiteOpenHelper를 통해 관리하고 있다. 상당히 row 레벨에서 컨트롤 중이고 버그도 존재하지 않지만, 과거 실력 부족으로 인해 보일러 플레이트 코드도 굉장히 많이 존재하고, 구조가 매우 좋지 않다. 그렇기에 Room 영속성 라이브러리로 앱을 관리하기로 결정했다.

 

Migration

Migration이란 안드로이드에서 DB의 변화가 발생할 때 수행하는 작업이다.

 

절차

 1. Room Entity 생성
 2. DAO 정의

 3. DB 정의

 

1. Room Entity 생성

이 단계에서 주의해야 할 점은 각 컬럼 별 데이터의 속성(제약, 디폴트값)이 같아야 한다. 그리고 테이블 명도 똑같이 정의해야 한다. 필자의 경우 type affinity를 맞추는 과정에서 많은 시간을 할애했다.

Room Entity와 테이블 타입 맞추기

@Entity(tableName = "weightTable")
data class BodyWeight(
        @PrimaryKey(autoGenerate = true) @ColumnInfo(name ="_id") var id: Int?,
        @ColumnInfo(name ="weight" )
        var bodyweight: Double,
        @ColumnInfo(name ="date", defaultValue = "(datetime('now','localtime'))")
        var date: String?
) {
    constructor(weight: Double,date: String) : this(null,weight,date) {}
}

2. DAO 정의

이 단계에선 앱에서 사용할 메서드를 정의하면 된다.

@Dao
interface BodyWeightDao {
    @Query("SELECT * FROM weightTable ORDER BY _id")
    fun getAll(): LiveData<List<BodyWeight>>

    @Query("SELECT * FROM weightTable WHERE date like :date limit 1")
    fun getCurrentDate(date:String): BodyWeight?
    @Insert
    fun insert(bodyWeight: BodyWeight)

    @Query("UPDATE weightTable SET weight=:weight WHERE date like :date")
    fun update(weight: Double, date: String)

    @Delete
    fun delete(bodyWeight: BodyWeight)

    @Query("DELETE FROM weightTable WHERE _id=:id")
    fun deleteById(id: Int)
}

 

3. DB 정의

기존 SQLiteOpenHelper에서 관리하던 DB 버젼이 1이었으므로 업그레이드를 위해 버젼을 2로 바꾼다. 다음으로 Migration 오브젝트를 정의해준다. 해당 코드에선 새로운 테이블을 만들고, 해당 테이블로 데이터를 복사한 뒤 기존 테이블을 삭제하고, 새로 생성한 테이블의 이름을 기존 테이블 명으로 재정의한다. 이 과정에서 아래 사이트에서 처럼 오류가 굉장히 많이 나타나게 된다. 이를 해결하기 위해선 CREATE TABLE 과정에서 속성을 잘 정의해야 하고, 앞서 정의한 1번의 entity도 잘 정의해야한다. stackoverflow.com/questions/53408207/room-migration-didnt-properly-handle

@Database(entities = [BodyWeight::class], version = 2)
abstract class BodyWeightDatabase() : RoomDatabase() {
    abstract fun BodyWeightDao(): BodyWeightDao

    companion object {
        private var INSTANCE: BodyWeightDatabase? = null
        private val sLock = Any()
        
        @VisibleForTesting
        val MIGRATION_1_2: Migration = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) {
                // Create the new table
                database.execSQL((
                        "CREATE TABLE weightTable_new (" +
                                "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                                "weight DOUBLE NOT NULL, " +
                                "date TEXT DEFAULT ((datetime('now','localtime'))))"))
                // Copy the data
                database.execSQL(("INSERT INTO weightTable_new (_id, weight, date) "
                        + "SELECT _id, weight, date "
                        + "FROM weightTable"))
                // Remove the old table
                database.execSQL("DROP TABLE weightTable")
                database.execSQL("ALTER TABLE weightTable_new RENAME TO weightTable");
            }
        }

        fun getInstance(context: Context): BodyWeightDatabase? {
            synchronized(sLock) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.applicationContext,
                            BodyWeightDatabase::class.java, "weight.db")
                            .build()
                }
                return INSTANCE
            }
        }
    }
}

해당 작업을 마치고 위에서 정의한 DB를 사용할 때 Migration이 된다.