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" />
Room 라이브러리는 @Database 주석이 달린 추상 클래스 내부에 정의된 추상 메서드에 대한 구현체를 자동으로 생성해줍니다.
getMyDao() 메서드가 abstract로 선언되어 있으며, 반환 타입이 MyDao인 인터페이스입니다.
이 메서드에 대한 구현체는 Room 라이브러리가 자동으로 생성해주므로, 개발자가 직접 구현할 필요가 없습니다.
따라서 MyDatabase.getDatabase(this).getMyDao() 구문을 통해 MyDao 인터페이스의 구현체를 얻을 수 있습니다.
이렇게 얻은 MyDao 인터페이스의 구현체를 통해 데이터베이스 작업을 수행할 수 있습니다.
이는 Room 라이브러리가 주석 처리기를 사용해 @Dao 주석이 달린 인터페이스에 대한 구현체를 생성하고,
이를 @Database 주석이 달린 클래스의 메서드를 통해 제공하기 때문에 가능한 것입니다.
이러한 과정을 통해 Room 라이브러리는 SQLite 데이터베이스 작업을 쉽게 수행할 수 있도록 도와줍니다.
MyDao 자체는 싱글턴이 아닙니다. MyDao는 인터페이스로, 이를 구현한 객체는
MyDatabase의 getMyDao() 메서드를 통해 얻을 수 있습니다. 그러나 MyDatabase가 싱글턴 패턴으로 구현되어 있다면,
MyDatabase의 인스턴스가 한 개만 존재하게 되므로, 이 인스턴스를 통해 얻은 MyDao 인스턴스는 실질적으로 싱글턴이 됩니다.
즉, MyDatabase.getDatabase(this).getMyDao()를 통해 얻은 MyDao 인스턴스는
항상 동일한 인스턴스를 반환하게 되므로, 이 MyDao 인스턴스는 싱글턴 패턴이 적용된 것과 같은 효과를 가집니다.
따라서 MyDao 인스턴스가 싱글턴인지 여부는 MyDatabase가 싱글턴 패턴으로 구현되어 있느냐에 달려 있습니다.
MyDatabase가 싱글턴 패턴으로 구현되어 있다면, MyDao 인스턴스도 싱글턴이 됩니다.