RoomDatabase 예제

jericho·2024년 1월 24일

Android

목록 보기
5/15

Room이란?
- SQLite를 쉽게 사용할 수 있는 데이터베이스 객체 매핑 라이브러리
- 쉽게 Query를 사용할 수 있는 API를 제공
- Query를 컴파일 시간에 검증함
- Query결과를 LiveData로하여 데이터베이스가 변경될 때 마다 쉽게 UI를 변경할 수 있음
- SQLite 보다 Room을 사용할 것을 권장함

// 그래들 plugins
    id("kotlin-kapt")

// 그래들 dependencies
    val roomVersion = "2.6.1"
    implementation("androidx.room:room-runtime:$roomVersion")
//    annotationProcessor("androidx.room:room-compiler:$room_version")  // kapt가 있으면 전혀 필요 없다
    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")

// Entity 생성 (데이터 클래스)
@Entity(tableName = "student_table")    // 테이블 이름을 student_table로 지정함
data class Student(
    @PrimaryKey @ColumnInfo(name = "student_id") val id: Int,
    val name: String
)

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

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

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

    @Query("SELECT * FROM student_table WHERE name = :sname")  // 메소드 인자를 SQL문에서 :을 붙여 사용
    suspend fun getStudentByName(sname: String): List<Student>
}

// Database 생성 (RoomDatabase 상속 추상클래스)
@Database(entities = [Student::class], version = 1, exportSchema = false)
abstract class MyDatabase : RoomDatabase() {
    // Room이 자동으로 구현해줌. 객체에 대해 단일로. MyDatabase가 싱글턴이면 얘도 싱글턴.
    abstract fun getMyDao(): MyDao

    companion object {
        @Volatile
        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")
            }
        }

        // TODO: 컨텍스트를 받을게 아니라, 애플리케이션컨텍스트로 고정해야 하는 것 아닌가?
        fun getDatabase(context: Context): MyDatabase {
            if (INSTANCE == null) synchronized(MyDatabase::class.java) {
                if (INSTANCE == null) INSTANCE = Room
                    .databaseBuilder(context, MyDatabase::class.java, "school_database")
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                    .build()
                // for in-memory database
                /*INSTANCE = Room.inMemoryDatabaseBuilder(
                    context, MyDatabase::class.java
                ).build()*/
            }
            return INSTANCE!!
        }
    }
}

// 액티비티에서
// TODO: 애플리케이션 컨텍스트를 줘야 하는 거 아닌가?
private val myDao by lazy { MyDatabase.getDatabase(this).getMyDao() }

// 이 아래는 onCreate에서 사용법에 관한 부분
// 쿼리문 결과에 대한 리스트 라이브데이터를 받아온건데 이게 변경되면 알림이 온다고? 신기하네
val allStudents = myDao.getAllStudents()
allStudents.observe(this) {
    val str = StringBuilder().apply {
        for ((id, name) in it) {
            append("$id-$name\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 { (id, name) ->
                    append("$id-$name")
                }
            }
            withContext(Dispatchers.Main) {
                binding.textQueryStudent.text = str
            }
        } else {
            withContext(Dispatchers.Main) {
                binding.textQueryStudent.text = ""
            }
        }
    }
}

// 메인 xml 참고용 (기본 바탕 컨스트레인트)
<EditText
    android:id="@+id/edit_student_id"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:ems="10"
    android:hint="ID"
    android:inputType="number"
    app:layout_constraintEnd_toStartOf="@+id/query_student"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintHorizontal_chainStyle="spread_inside"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<EditText
    android:id="@+id/edit_student_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:ems="10"
    android:hint="student name"
    android:inputType="textPersonName"
    app:layout_constraintEnd_toStartOf="@+id/add_student"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintHorizontal_chainStyle="spread_inside"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/edit_student_id" />

<Button
    android:id="@+id/add_student"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Add Student"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toEndOf="@+id/edit_student_name"
    app:layout_constraintTop_toBottomOf="@+id/query_student" />

<Button
    android:id="@+id/query_student"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Query Student"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toEndOf="@+id/edit_student_id"
    app:layout_constraintTop_toTopOf="parent" />

<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    android:text="Result of Query Student"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/edit_student_name" />

<TextView
    android:id="@+id/textView2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="24dp"
    android:text="Student List"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/text_query_student" />

<TextView
    android:id="@+id/text_query_student"
    android:layout_width="0dp"
    android:layout_height="100sp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/textView" />

<TextView
    android:id="@+id/text_student_list"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_marginBottom="8dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/textView2" />
  • getMyDao가 abstract인데 상속 후 구현이 안되어있는데 어떻게 그냥 쓰지?

Room 라이브러리는 @Database 주석이 달린 추상 클래스 내부에 정의된 추상 메서드에 대한 구현체를 자동으로 생성해줍니다.
getMyDao() 메서드가 abstract로 선언되어 있으며, 반환 타입이 MyDao인 인터페이스입니다.
이 메서드에 대한 구현체는 Room 라이브러리가 자동으로 생성해주므로, 개발자가 직접 구현할 필요가 없습니다.
따라서 MyDatabase.getDatabase(this).getMyDao() 구문을 통해 MyDao 인터페이스의 구현체를 얻을 수 있습니다.
이렇게 얻은 MyDao 인터페이스의 구현체를 통해 데이터베이스 작업을 수행할 수 있습니다.
이는 Room 라이브러리가 주석 처리기를 사용해 @Dao 주석이 달린 인터페이스에 대한 구현체를 생성하고,
이를 @Database 주석이 달린 클래스의 메서드를 통해 제공하기 때문에 가능한 것입니다.
이러한 과정을 통해 Room 라이브러리는 SQLite 데이터베이스 작업을 쉽게 수행할 수 있도록 도와줍니다.

  • 그러면 MyDao는 싱글턴이야?

MyDao 자체는 싱글턴이 아닙니다. MyDao는 인터페이스로, 이를 구현한 객체는
MyDatabasegetMyDao() 메서드를 통해 얻을 수 있습니다. 그러나 MyDatabase가 싱글턴 패턴으로 구현되어 있다면,
MyDatabase의 인스턴스가 한 개만 존재하게 되므로, 이 인스턴스를 통해 얻은 MyDao 인스턴스는 실질적으로 싱글턴이 됩니다.
즉, MyDatabase.getDatabase(this).getMyDao()를 통해 얻은 MyDao 인스턴스는
항상 동일한 인스턴스를 반환하게 되므로, 이 MyDao 인스턴스는 싱글턴 패턴이 적용된 것과 같은 효과를 가집니다.
따라서 MyDao 인스턴스가 싱글턴인지 여부는 MyDatabase가 싱글턴 패턴으로 구현되어 있느냐에 달려 있습니다.
MyDatabase가 싱글턴 패턴으로 구현되어 있다면, MyDao 인스턴스도 싱글턴이 됩니다.

0개의 댓글