Android/StoreInfo

<강의> 안드로이드 앱 개발 심화 - Room

re트 2024. 1. 23. 17:16
728x90

1. Room이란?

- SQLite를 쉽게 사용할 수 있는 데이터베이스 객체 매핑 라이브러리

- 쉽게 Query를 사용할 수 있는 API를 제공

- Query를 컴파일 시간에 검증 (SQLite보다 Room 사용을 권장하는 이유 중 하나)

- Query 결과를 LiveData로 하여 데이터베이스가 변경될 때마다 쉽게 UI 변경 가능

 

2. Room의 주요 3요소

1) @Database(entities = [포함되는 Entity 리스트], version = Int형 숫자)

- RoomDatabase를 상속받은 클래스를 데이터베이스로 지정하는 어노테이션

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

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

- DAO를 가져올 수 있는 getter 메소드 작성

- 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) { 
                database.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
            }
        }

        private val MIGRATION_2_3 = object : Migration(2, 3) {
            override fun migrate(database: SupportSQLiteDatabase) { 
                database.execSQL("ALTER TABLE class_table ADD COLUMN last_update INTEGER")
            }
        }
        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
        }
    }
}

2) @Entity(tableName = "사용할 테이블 이름")

- 클래스(데이터 클래스)를 테이블 스키마로 지정하는 어노테이션

- 클래스의 변수들이 Column이 됨

- 포함되는 어노테이션

  > @PrimaryKey : 기본키 설정하는 어노테이션이며 보통 고유한 id 값으로 설정

  > @ColumnInfo : 테이블 Column을 변수와 매칭 / Column의 이름, 타입, 인덱스 등을 설정 가능

@Entity(tableName = "student_table")
data class Student (
    @PrimaryKey 
    @ColumnInfo(name = "student_id") 
    val id: Int,
    val name: String
)

3) @Dao

- 클래스(인터페이스나 추상 클래스)를 DAO(데이터 접근 객체)로 지정하는 어노테이션

- 기본적인 insert, delete, update SQL은 컴파일러가 자동으로 만들어줌

- 복잡한 SQL은 직접 만들 수 있음

- 포함되는 어노테이션

  > @Insert : key가 중복되는 경우의 처리를 위해 onConflict 지정 가능

  > @Update : key가 중복되는 경우의 처리를 위해 onConflict 지정 가능, 기본키에 해당되는 튜플을 찾아서 변경

  > @Delete : 기본키에 해당되는 튜플을 찾아서 삭제

  > @Query : SQL 쿼리 정의하고 그 쿼리를 위한 메소드 선언

    >> OnConflictStrategy.ABORT : key 충돌시 종료

    >> OnConflictStrategy.IGNORE : key 충돌시 무시

    >> OnConflictStrategy.REPLACE : key 충돌시 새로운 데이터로 변경

- @Query로 리턴되는 데이터의 타입을 LiveData<>로 하면 데이터가 업데이트될 때 옵저버를 통해 바로 알 수 있음

- @Query에 SQL을 정의할 때 메소드의 인자를 사용 가능

@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는 코틀린 코루틴을 사용하는 것이기 때문에 runBlocking {} 내에서 호출해야함
    suspend fun getStudentByName(sname: String): List<Student>

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

    // ...
}

 

- 이외의 어노테이션

  > @Transaction : 메소드가 하나의 트랜잭션으로 실행되어야 함을 나타냄 / 여러 연산을 하나의 작업으로 묶어 실행할 때 사용

  > @ForeignKey : 엔티티 간의 외래키 관계를 정의할 때 사용 / 참조 무결성을 유지하는데 도와줌

  > @Index : 특정 Column에 인덱스를 생성할 때 사용 / 쿼리 성능을 향상시키는데 유용

 

3. gradle 파일 설정

- build.gradle(Module :app)의 plugins과 dependencies에 해당 코드를 추가해야함

plugins {
    ...
    id("kotlin-kapt")
}

dependencies {
    ...

    val roomVersion = "2.6.1"
    implementation("androidx.room:room-runtime:$roomVersion")
    annotationProcessor("androidx.room:room-compiler:$roomVersion")
    kapt("androidx.room:room-compiler:$roomVersion")
    // optional - Kotlin Extensions and Coroutines support for Room
    implementation("androidx.room:room-ktx:$roomVersion")
    // optional - Test helpers
    testImplementation("androidx.room:room-testing:$roomVersion")
}

- kapt(Kotlin Annotation Processing Tool)를 사용하면 Kotlin 코드에서 자바 어노테이션 프로세서를 사용할 수 있다.

 

4. UI와 연결

- RoomDatabase 객체에서 DAO 객체를 받아오고, 이 DAO 객체의 메소드를 호출하여 데이터베이스 접근

- LiveData<> 타입으로 리턴되는 DAO 메소드의 경우 observe() 메소드를 이용하여 옵저버 지정하여 데이터가 변경될 때마다 옵저버의 onChanged()가 호출됨

- LiveData<> 타입으로 리턴되는 DAO 메소드의 경우 옵저버를 통해 비동기적으로 데이터를 받기 때문에 UI 스레드에서 직접 호출해도 문제 없음

 

5. 데이터베이스 생성 확인

- 하단의 App Inspection -> Database Inspector -> 데이터베이스명 -> 테이블명

 

6. 예제

1) build.gradle

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("kotlin-kapt")
}

android {
    ...
    
    buildFeatures {
        viewBinding = true
    }
}

dependencies {

    implementation("androidx.core:core-ktx:1.9.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    implementation("androidx.fragment:fragment-ktx:1.6.2")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

    val roomVersion = "2.6.1"
    implementation("androidx.room:room-runtime:$roomVersion")
    annotationProcessor("androidx.room:room-compiler:$roomVersion")
    kapt("androidx.room:room-compiler:$roomVersion")
    // optional - Kotlin Extensions and Coroutines support for Room
    implementation("androidx.room:room-ktx:$roomVersion")
    // optional - Test helpers
    testImplementation("androidx.room:room-testing:$roomVersion")
}

 

2) Student.kt(Entity)

@Entity(tableName = "student_table")
data class Student(
    @PrimaryKey
    @ColumnInfo(name = "student_id")
    val id: Int,
    val name: String
)

 

3) MyDAO.kt(Dao)

@Dao
interface MyDAO {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertStudent(student: Student)

    @Query("SELECT * From student_table")
    fun getAllStudents(): LiveData<List<Student>>

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

    @Delete
    suspend fun deleteStudent(student: Student)
}

 

4) MyDatabase.kt(Database)

@Database(entities = [Student::class], exportSchema = false, 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(db: SupportSQLiteDatabase) {
            }
        }

        private val MIGRATION_2_3 = object : Migration(2, 3) {
            override fun migrate(db: SupportSQLiteDatabase) {
                db.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
            }
        }

        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
        }
    }
}

 

5) MainActivity.kt

class MainActivity : AppCompatActivity() {

    private val binding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    lateinit var myDao: MyDAO

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        myDao = MyDatabase.getDatabase(this).getMyDao()

        val allStudents = myDao.getAllStudents()
        allStudents.observe(this) {
            val str = StringBuilder().apply {
                for ((id, name) in it) {
                    append(id)
                    append("-")
                    append(name)
                    append("\n")
                }
            }.toString()
            binding.textStudentList.text = str
        }

        binding.addStudent.setOnClickListener {
            val id = binding.editStudentId.text.toString().toInt()
            val name = binding.editStudentName.text.toString()
            if (id > 0 && name.isNotEmpty()) {
                CoroutineScope(Dispatchers.IO).launch {
                    myDao.insertStudent(Student(id, name))
                }
            }

            binding.editStudentId.text = null
            binding.editStudentName.text = null
        }

        binding.queryStudent.setOnClickListener {
            val name = binding.editStudentName.text.toString()
            CoroutineScope(Dispatchers.IO).launch {
                val results = myDao.getStudentByName(name)

                if (results.isNotEmpty()) {
                    val str = StringBuilder().apply {
                        results.forEach { student ->
                            append(student.id)
                            append("-")
                            append(student.name)
                        }
                    }
                    withContext(Dispatchers.Main) {
                        binding.textQueryStudent.text = str
                    }
                } else {
                    withContext(Dispatchers.Main) {
                        binding.textQueryStudent.text = ""
                    }
                }
            }
        }
    }
}
반응형