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 = ""
}
}
}
}
}
}'Android > StoreInfo' 카테고리의 다른 글
| <정리> 심화 개인과제 1 (0) | 2024.01.24 |
|---|---|
| <강의> 안드로이드 앱 개발 심화 - 사용자 위치 얻기 (1) | 2024.01.24 |
| <강의> 안드로이드 앱 개발 심화 - SharedPreferences (1) | 2024.01.23 |
| <정리> 챌린지반 과제2 - 2 (0) | 2024.01.23 |
| <정리> 챌린지반 과제2 - 1 (0) | 2024.01.23 |