Room이란?
Room은 스마트폰 내장 DB에 데이터를 저장하기 위해 사용하는 라이브러리
Room은 완전히 새로운 개념은 아니고, SQLite를 활용해서 객체 매핑을 해주는 역할을 한다.
Room 구조
Entity
- 개체
- 관련 있는 속성들이 모여 하나의 정보 단위
@Entity
data class User (
var name: String,
var age: String,
var phone: String
){
@PrimaryKey(autoGenerate = true) var id: Int = 0
}
DAO
데이터에 접근할 수 있는 메서드를 정의 해놓은 인터페이스
@Dao
interface UserDao {
@Insert
fun insert(user: User)
@Update
fun update(user: User)
@Delete
fun delete(user: User)
}
@Dao
interface UserDao {
@Query("SELECT * FROM User") // 테이블의 모든 값을 가져와라
fun getAll(): List<User>
@Query("DELETE FROM User WHERE name = :name") // 'name'에 해당하는 유저를 삭제해라
fun deleteUserByName(name: String)
}
Room Database
@Database(entities = [User::class], version = 1)
abstract class UserDatabase: RoomDatabase() {
abstract fun userDao(): UserDao
}
@Database(entities = arrayOf(User::class, Student::class), version = 1)
abstract class UserDatabase: RoomDatabase() {
abstract fun userDao(): UserDao
}
데이터베이스를 생성하고 관리하는 데이터베이스 객체 만들기 위해서 위와 같은 추상 클래스를 만들어 줘야 한다.
- RoomDatabase 클래스를 상속받고, @Database 어노테이션으로 데이터베이스임을 표시
version은 앱을 업데이트하다가 entity의 구조를 변경해야 하는 일이 생겼을 때 이전 구조와 현재 구조를 구분해주는 역할
여러 개의 entity를 가져야 한다면 arrayOf() 안에 콤마로 구분해서 entity를 넣어주면 된다.
데이터베이스 객체를 인스턴스 할 때 싱글톤으로 구현하기를 권장하고 있다.
@Database(entities = [User::class], version = 1)
abstract class UserDatabase: RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
private var instance: UserDatabase? = null
@Synchronized
fun getInstance(context: Context): UserDatabase? {
if (instance == null) {
synchronized(UserDatabase::class){
instance = Room.databaseBuilder(
context.applicationContext,
UserDatabase::class.java,
"user-database"
).build()
}
}
return instance
}
}
}
실제 사용
var newUser = User("김똥깨", "20", "010-1111-5555")
// 싱글톤 패턴을 사용하지 않은 경우
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"user-database"
).build()
db.UserDao().insert(newUser)
// 싱글톤 패턴을 사용한 경우
val db = UserDatabase.getInstance(applicationContext)
db!!.userDao().insert(newUser)
저대로 실행하면 "Cannot access database on the main thread since it may potentially lock the UI for a long period of time" 에러가 뜬다.
- 비동기 실행하면 된다고 함.
해결 법
- allowMainThreadQueries()를 사용해 강제로 실행
- 비동기 실행을 하면 된다.
allowMainThreadQueries는 안드로이드 Room DB 설정 시 메인 스레드에서 데이터베이스 쿼리를 허용하도록 하는 옵션
장점:
- 간편한 사용: 메인 스레드에서 직접 데이터베이스 작업을 수행할 수 있어 코드 작성이 간편해집니다.
- 빠른 프로토타이핑: 초기 개발 단계에서 빠르게 기능을 구현하고 테스트하는 데 유용할 수 있습니다.
단점:
- 앱 성능 저하: 메인 스레드는 UI 스레드이므로, 이곳에서 긴 데이터베이스 작업을 수행하면 앱이 느려지거나 응답하지 않게 될 수 있습니다.
- 안정성 문제: 메인 스레드에서 무거운 작업을 하면 ANR(Application Not Responding) 오류가 발생할 위험이 있습니다.
권장 사항:
- 비동기 작업 사용: 메인 스레드 대신 AsyncTask, Executor, LiveData, RxJava, Kotlin Coroutines 등을 사용하여 백그라운드 스레드에서 데이터베이스 작업을 수행하는 것이 좋습니다.
- 테스트 목적 사용: allowMainThreadQueries는 테스트나 프로토타이핑 단계에서만 사용하고, 실제 프로덕션 코드에서는 비동기 작업으로 대체해야 합니다.
import android.content.Context
import androidx.room.Room
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
지피티에서 사용법
1. 데이터베이스 설정
import android.content.Context
import androidx.room.Room
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
2. DAO 인터페이스
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao
interface EmoticonDao {
@Query("SELECT * FROM EmoticonList")
suspend fun getAllEmoticons(): List<Emoticon>
@Insert
suspend fun insertEmoticon(emoticon: Emoticon)
}
3. Repository 클래스
class EmoticonRepository(private val emoticonDao: EmoticonDao) {
val allEmoticons: LiveData<List<Emoticon>> = emoticonDao.getAllEmoticons()
suspend fun insert(emoticon: Emoticon) {
emoticonDao.insertEmoticon(emoticon)
}
}
4. ViewModel 클래스
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class EmoticonViewModel(private val repository: EmoticonRepository) : ViewModel() {
val allEmoticons: LiveData<List<Emoticon>> = repository.allEmoticons
fun insert(emoticon: Emoticon) = viewModelScope.launch {
repository.insert(emoticon)
}
}
Room 마이그레이션
1. 데이터 베이스 버전 관리
Room 데이터베이스의 버전을 관리하고, 마이그레이션을 정의해야 합니다. 데이터베이스의 버전은 AppDatabase 클래스에 지정
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [Emoticon::class], version = 2)
abstract class AppDatabase : RoomDatabase() {
abstract fun emoticonDao(): EmoticonDao
}
2. 마이그레이션 정의
새로운 버전으로의 마이그레이션 로직을 작성해야 합니다.
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// 여기서 데이터베이스 스키마 변경 작업을 수행합니다.
// 예시: 새로운 컬럼 추가
database.execSQL("ALTER TABLE EmoticonList ADD COLUMN new_column TEXT")
}
}
3. Room 데이터베이스 빌더에 마이그레이션 추가
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).addMigrations(MIGRATION_1_2)
.build()
4. 마이그레이션 테이블 추가 작업
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// EmoticonDetails 테이블 생성
database.execSQL("""
CREATE TABLE IF NOT EXISTS EmoticonDetails (
id INTEGER PRIMARY KEY NOT NULL,
emoticonId INTEGER NOT NULL,
description TEXT NOT NULL,
imageUrl TEXT NOT NULL
)
""".trimIndent())
}
}
실제 마이그레이션시 오류 날 경우
- 실제 로그가 찍히지는 않는것 같다.
- RoomOpenHelper.java 파일에 마이그레이션 하는쪽 onUpgrade 디버깅을 하여 메시지를 본다.
- 그 다음 지피티에게 물어봐서 어떤게 잘못 되었는지 확인하기
- 나는 version필드명이 서로 다르게 입력 되어 있었음.
DB 활용법 (Select, Insert, Update)
Select
Dao에 관련 쿼리 함수들을 넣는다.
@Query("SELECT * FROM emoticon_list WHERE user_id = :userId")
fun getEmoticonList(userId: String): EmoticonListEntity?
Insert
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertEmoticons(emoticons: List<EmoticonListEntity>): List<Long>
- OnConflictStrategy.REPLACE : 삽입(insert)이나 업데이트(update) 시 발생할 수 있는 충돌 상황에서 사용할 수 있는 전략
OnConflictStrategy.REPLACE 동작 방식
- 삽입하려는 데이터의 키(Primary Key)가 기존 데이터와 충돌하는 경우: 기존 행을 삭제하고 새로운 데이터를 삽입합니다.
- 충돌하지 않는 경우: 데이터를 그냥 삽입합니다.
다른 충돌 처리 전략
- IGNORE: 충돌이 발생한 경우 삽입을 무시합니다.
- ABORT: 충돌이 발생한 경우 트랜잭션을 중단하고 롤백합니다.
- FAIL: 충돌이 발생한 경우 트랜잭션을 중단하지만 롤백하지 않습니다.
- ROLLBACK: 충돌이 발생한 경우 트랜잭션을 롤백합니다.
결과 List<Long> : 삽입된 각 엔티티의 행 ID(row ID)
ex)
val emoticons = listOf(
EmoticonListEntity(id = 1, name = "Smile"),
EmoticonListEntity(id = 2, name = "Sad"),
EmoticonListEntity(id = 1, name = "Happy") // This will replace the first one
)
val ids = emoticonDao.insertEmoticons(emoticons)
println(ids) // Output will be [1, 2, 1]
Update
@Update
fun updateEmoticon(emoticon: EmoticonListEntity): Int
- Int는 update된 개수라고 한다.
- primary key가 같은게 없어서 업데이트가 되지 않으면 0
Join
쿼리 관련 테이블 Join을 해야하는 경우가 생긴다.
@Query("SELECT * FROM emoticon_detail INNER JOIN emoticon_detail_image ON emoticon_detail.id = emoticon_detail_image.emoticon_id WHERE emoticon_detail.id = :emoticonId")
- INNER JOIN은 두 테이블 간의 일치하는 행만 가져오는 조인 방식입니다.
- ON 절은 두 테이블 간의 조인 조건을 지정합니다. 이 경우 emoticon_detail 테이블의 id 필드와 emoticon_detail_image 테이블의 emoticon_id 필드가 동일할 때 조인을 수행합니다.
- WHERE 절은 조인된 결과에서 특정 조건을 만족하는 데이터를 선택
더 쉽게 해결할 수 있는 방법이 있다.
@Relation
테이블을 조인한 결과를 담을 새로운 데이터 클래스를 정의.
- @Relation 어노테이션은 Room에서 사용되어 관계형 데이터베이스의 테이블 간 관계를 표현
- 부모 엔티티와 자식 엔티티(또는 연결된 엔티티) 사이의 관계를 정의할 때 사용됩니다. 주로 한 엔티티에 대해 여러 개의 관련된 엔티티를 포함하는 경우에 사용됩니다.
- @Relation 어노테이션이 사용된 메서드에서는 @Transaction 어노테이션을 함께 사용하는 것이 권장된다고 한다.
@Transaction
- @Transaction 어노테이션은 Room에서 트랜잭션 단위로 데이터베이스 작업을 처리할 때 사용됩니다.
@Transaction 어노테이션의 역할
트랜잭션 단위로 묶음:
@Transaction 어노테이션은 여러 개의 데이터베이스 작업을 원자적(atomic)으로 묶어주는 역할을 합니다.
이는 모든 작업이 성공적으로 완료될 때만 변경 사항을 커밋하고, 하나라도 실패하면 롤백하여 이전 상태로 되돌리는 기능을 제공합니다.
@Relation 을 이용한 ‘결과 Entity’
data class EmoticonDetailResultEntity(
@Embedded val emoticonDetail: EmoticonDetailEntity,
@Relation(
parentColumn = "id",
entityColumn = "emoticon_id"
)
val images: List<EmoticonDetailImageEntity>
)
// 실제 쿼리 한번으로 두 테이블 데이터를 가져올 수 있다.
@Transaction
@Query("SELECT * FROM emoticon_detail WHERE id = :emoticonId")
fun getEmoticonDetailWithImage(emoticonId: Int): EmoticonDetailResultEntity?
작업 하면서 궁금 했던 것
Select할 때 @Relation으로 묶어서 Select했는데 Delete할 때에도 id로 두 테이블 데이터 삭제가 가능한지?
Room에서는 관계를 맺은 두 테이블 데이터 삭제할 때 외래 키 제약 조건과 CASCADE 옵션을 사용하여 하나의 테이블에서 삭제할 때 자동으로 관련된 다른 테이블 데이터도 삭제 가능
- 외래키를 설정하고 CASCADE 삭제를 활성화 해야 한다.
Room에서는 기본적으로 비활성화
- 데이터베이스 빌더에서 setForeignKeyConstraintsEnabled(true)를 호출 해야 한다.
@Entity(
tableName = "emoticon_detail_image",
foreignKeys = [ForeignKey(
entity = EmoticonDetailEntity::class,
parentColumns = ["id"],
childColumns = ["emoticon_id"],
onDelete = ForeignKey.CASCADE
)]
)
data class EmoticonDetailImageEntity(
@PrimaryKey @ColumnInfo(name = "id") val imageId: Int,
@ColumnInfo(name = "emoticon_id") val emoticonId: Int,
val name: String? = null,
val ordering: Int,
val url: String? = null,
val thumbnail: String? = null,
val version: String? = null
)
데이터 베이스 빌더 설정
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "MyDatabase.db")
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
db.execSQL("PRAGMA foreign_keys=ON")
}
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
db.execSQL("PRAGMA foreign_keys=ON")
}
})
.build()
DAO 삭제 메서드 추가
emoticonId로 삭제하는 DAO 메서드를 작성합니다.
CASCADE 옵션을 사용했으므로 EmoticonDetailEntity를 삭제할 때 자동으로 관련된 EmoticonDetailImageEntity도 삭제됩니다.
@Dao
interface EmoticonDao {
@Query("DELETE FROM emoticon_detail WHERE id = :emoticonId")
suspend fun deleteEmoticonById(emoticonId: Int)
}
Room에서는 한 번에 여러 테이블에 데이터를 삽입하는 기능을 제공하지 않음.
하나의 트랜잭션 내에서 여러 테이블에 데이터를 삽입할 수 있습니다.
@Transaction
suspend fun insertEmoticonDetailWithImages(
emoticonDetail: EmoticonDetailEntity,
emoticonDetailImages: List<EmoticonDetailImageEntity>
) {
insertEmoticonDetail(emoticonDetail)
insertEmoticonDetailImages(emoticonDetailImages)
}
출처 :
https://todaycode.tistory.com/39
https://android-dev.tistory.com/entry/AndroidKotlin-안드로이드-Room-Database-사용하기3-Migration
'안드로이드' 카테고리의 다른 글
[Android/iOS] 웹뷰 브릿지 함수 JsonArray를 String으로 전달 할 때 이슈 (0) | 2024.11.18 |
---|---|
[Android] 로컬 알람 적용 방법 (0) | 2024.11.14 |
[Android] CoroutineWorker와 Hilt를 사용한 파일 다운로드 및 압축 해제 (0) | 2024.06.14 |
[Android] Glide를 통해 이모티콘 Gif 구현하기 (0) | 2024.06.14 |
[Android] WebView 텍스트 고정 (글자크기 고정) (0) | 2024.05.28 |