본문 바로가기
Android Studio

[Android 앱개발 심화] 데이터 저장 (3) - Room (3요소 생성, Migration) (# 추가하기)

by 젼젼39 2024. 4. 30.

1. Entity 생성

: Entity 는 테이블 스키마 정의

    //모든 데이터베이스는 테이블들을 가지고 있고, 그 테이블들이 어떤 컬럼들을 갖고있을지를 정의하는 게 스키마

    //테이블을 만드는 쿼리문이 CREATE TABLE

    // student_table이라는 이름으로 테이블을 하나 만들건데, 그 안에 student_id가 들어가고 name이 들어갈거다.
    // student_id는 integer이고, primary key(유니크한, 중복되지 않는 값)를 가질거다.
    // name은 text 타입인데, not null이다. null이 들어갈 수 없다.

- CREATE TABLE student_table (student_id INTEGER PRIMARY KEY, name TEXT NOT NULL);

- @Entity data class Student

@Entity(tableName = "student_table")    // 테이블 이름을 student_table로 지정함
//data class를 student라는 이름으로 하나를 만드는데, 
data class Student (
    @PrimaryKey 
    //이 테이블 명이 student_table이고, 
    //  columninfo에 name이 들어가는데 name이 "student_id"이다.
    @ColumnInfo(name = "student_id") 
    //2개의 컬럼이 들어가게 됨, id와 name
    val id: Int,
    val name: String
)

 

2. DAO 생성

- DAO는 interfaceabstract class로 정의되어야 함

- Annotation에 SQL 쿼리를 정의하고 그 쿼리를 위한 메소드를 선언

- 가능한 annotation으로 @Insert, @Update, @Delete, @Query 가 있음

@Query("SELECT * from table") fun getAllData() : List<Data>
//테이블 안의 모든 데이터를 가져오라는 뜻

- @Insert, @Update, @Delete는 SQL 쿼리를 작성하지 않아도 컴파일러가 자동으로 생성함

    - @Insert나 @Update는 key가 중복되는 경우 처리를 위해 onConflict를 지정할 수 있음

OnConflictStrategy.ABORT key 충돌 시 종료
OnConflictStrategy.IGNORE key 충돌 무시
OnConflictStrategy.REPLACE key 충돌 시 새로운 데이터로 변경

    - @Update나 @Delete는 primary key에 해당되는 튜플을 찾아서 변경/삭제함

- @Query 로 리턴되는 데이터의 타입을 LiveData<>로 하면, 나중에 이 데이터가 업데이트될 때 Observer를 통해 알 수 있음

@Query("SELECT * from table") fun getAllData() : LiveData<List<Data>>

- @Query에 SQL을 정의할 때 메소드의 인자를 사용할 수 있음

//student_table에서 모든 데이터를 갖고올건데, name이 sname인 것을 찾아줘라.

@Query("SELECT * FROM student_table WHERE name = :sname")
suspend fun getStudentByName(sname: String): List<Student>

    // 인자 sname을 SQL에서 :sname으로 사용

- fun 앞에 suspend는 Kotlin coroutine을 사용하는 것... 나중에 이 메소드를 부를 때에는 runBlocking { } 안에서 호출해야 함

- LiveData는 비동기적으로 동작하기 때문에 coroutine으로 할 필요가 없음

@Dao
interface MyDAO {
    @Insert(onConflict = OnConflictStrategy.REPLACE)  // INSERT, key 충돌이 나면 새 데이터로 교체
    suspend fun insertStudent(student: Student)

    @Query("SELECT * FROM student_table")
    fun getAllStudents(): LiveData<List<Student>>        // LiveData<> 사용

    @Query("SELECT * FROM student_table WHERE name = :sname")   
    suspend fun getStudentByName(sname: String): List<Student>

    @Delete
    suspend fun deleteStudent(student: Student); // primary key is used to find the student

    // ...
}

 - @Query("SELECT * from table") fun getAllData() : LiveData<List<Data>>

@Query("SELECT * FROM student_table WHERE name = :sname")
suspend fun getStudentByName(sname: String): List<Student>

- 인자 sname을 SQL에서 :sname으로 사용

 

 

3. Database 생성

- RoomDatabase를 상속해 자신의 Room 클래스를 만들어야 함

- 포함되는 Entity들데이터베이스 버전(version)을 @Database annotation에 지정함

    - version이 기존에 저장되어 있는 데이터베이스보다 높으면, 데이터베이스를 open할 때 migration을 수행하게 됨

    - Migration 수행 방법은 RoomDatabase 객체의 addMigration() 메소드를 통해 알려줌

//Migration을 통해 기존에 깔려있던 앱에 테이블을 추가... 

- DAO를 가져올 수 있는 getter 메소드를 만든다

    - 실제 메소드 정의는 자동으로 생성됨

- Room 클래스의 인스턴스는 하나만 있으면 되니까 Singleton 패턴을 사용함

- Room 클래스의 객체 생성은 Room.databaseBuilder() 를 이용

@Database(entities = [Student::class, ClassInfo::class, Enrollment::class, Teacher::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
    abstract fun getMyDao() : MyDAO

    companion object {
        private var INSTANCE: MyDatabase? = null
        private val MIGRATION_1_2 = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) { 생략 }
        }

        private val MIGRATION_2_3 = object : Migration(2, 3) {
            override fun migrate(database: SupportSQLiteDatabase) { 생략 }
        }
        fun getDatabase(context: Context) : MyDatabase {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(
                    context, MyDatabase::class.java, "school_database")
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                    .build()
            }
            return INSTANCE as MyDatabase
        }
    }
}

 

4. Migration

- 앞에서 MyRoomDatabae 객체 생성 후 addMigrations() 메소드를 호출해 Migration 방법을 지정했음

    - 여러개의 Migration 지정 가능

//Migration을 통해 기존에 깔려있던 앱에 테이블을 추가... 

Room.databaseBuilder(...).addMigrations(MIGRATION_1_2, MIGRATION_2_3)

private val MIGRATION_1_2 = object : Migration(1, 2) {   // version 1 -> 2
    override fun migrate(database: SupportSQLiteDatabase) {
    	//ALTER : 테이블을 변경하는 것
        //  ADD COLUMN : 컬럼을 하나? 추가한다
        database.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
    }
}

private val MIGRATION_2_3 = object : Migration(2, 3) {   // version 2 -> 3
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE class_table ADD COLUMN last_update INTEGER")
    }
}