#Room Android Developer 공식문서
=>https://developer.android.com/training/data-storage/room?hl=ko
(여기에 들어가면 좌측에 "로컬 데이터베이스에 데이터 저장" 하단에 Room에 대한 정보가 많음)
-큰 규모의 데이터를 처리해야 하는 앱은 데이터를 로컬에 유지하면 휴대폰을 네트워크에 연결하지 않아도 사용자가 휴대폰의 데이터를 탐색할 수 있는 장점이 있다. Room은 SQLite의 장점은 받아들이면서 SQLite의 단점(컴파일시 발생하는 에러 확인X , boilerplate 코드)은 상쇄시킨 SQLite 위에 구축된 라이브러리로 우리에게 더 높은 편의성을 제공해준다.
-Room을 사용하기 위해서는 모듈수준의 build.gradle에 종석성을 아래와 같이 추가해주어야 한다. 아래는 기본적인 Room 관련 종속성 추가이고 Ksp를 쓰거나 Pagging , Coroutine 등등과 함께 사용하려면 추가적인 종속성 추가를 해주어야 한다.
implementation("androidx.room:room-ktx:${room_version}")
kapt("androidx.room:room-compiler:${room_version}")
// 아래는 추가적인 Room 관련 종속성 추가 내용임.
ksp("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$room_version")
testImplementation("androidx.room:room-testing:$room_version")
implementation("androidx.room:room-paging:$room_version")
-Room의 주요 구성요소는 총 3가지가 존재한다.
[1. 데이터베이스 클래스 : 데이터 베이스를 실제로 가지고 있다. + 기본 접근 역할].
[2. 데이터 항목 : 앱의 데이터베이스의 테이블의 행 하나를 나타낸다. + 고유의 접근 키값이 있다.].
[3. 데이터 엑세스 객체(DAO) : 앱에서 데이터베이스의 데이터를 처리 할 수 있는 메서드를 제공한다.]
-AppInspection을 클릭하면 안드로이드 스튜디오 내부에서 현재 에뮬레이터 기기를 누르면 데이터베이스 검사를 할 수 있다.
-Database Class는 데이터베이스를 보유하는 역할과 RoomDatabase에 대한 구성을 정의하고 영구 데이터에 대한 앱의 기본 엑세스 지점 역할도 한다.
-Database Class는 3가지의 조건을 충족해야 한다.
Database Class는 RoomDatabase와 연결된 데이터 항목을 모두 나열하는 entities 배열이 포함된 @Database 주석이 달려야 한다.
Database Class는 RoomDatabase를 확장하는 추상 클래스여야 한다.
Database Class는 앱이 사용할 각 테이블에 대한 데이터 접근 객체(DAO)를 제공하는 Property를 포함해야 하며, 이 Property는 매개변수 없이 정의되어 DAO 객체의 인스턴스를 반환하거나 DAO 객체 타입이어야 한다.
-abstract class ContactDatabase가 RoomDatabase임을 Android RoomLibrary에게 알려서 실제 Database를 생성하기 위해서 Database Annotation을 사용하는 것이다.
-Database Annotation의 Parameter인 entities에는 RoomDatabase에서 사용 할 테이블을 정의한 클래스를 넣어주면 된다.
-Database Annotation의 Parameter인 version에는 RoomDatabase의 버전을 지정해주면 된다.
-RoomDatabase의 작업을 수행하는데 필요한 코드를 자동으로 만들어주기 위해서 RoomDatabase()를 상속 받는다.
-아래는 위 내용을 기반으로 Database Class를 만든 예시코드이다.
>abstract class ContactDatabase가 RoomDatabase임을 Android RoomLibrary에게 알려서
실제 Database를 생성하게 하고 싶으면 Database Annotation을 사용하면 된다.
>Database Annotation의 Parameter인 entities에는 RoomDatabase에서 사용 할 테이블을 정의한
클래스를 넣어주면 된다.
>Database Annotation의 Parameter인 version에는 RoomDatabase의 버전을 지정해주면 된다.
@Database(
entities = [Contact::class],
version = 1
)
>RoomDatabase의 작업을 수행하는데 필요한 코드를 자동으로 만들어주기 위해서 RoomDatabase()를\
상속 받는다.
abstract class ContactDatabase: RoomDatabase() {
// RoomDatabase 작업을 위해서 정의한 Dao 인터페이스 타입의 추상 변수를 만든다.
abstract val dao: ContactDao
}
-RoomLibrary를 사용하여 Data를 저장할 때는 저장하려는 객체를 나타내는 항목 , 즉, 테이블이라고 불리는 것을 정의해주어야 한다.
-@Entity
-데이터 테이블의 행은 data class 객체가 들어가고 , 데이터 테이블의 열은 data class의 각각의 속성들이 들어간다. 이를 RoomLibrary에게 알려주기 위해서는 Entity Annotation을 data class 위에 작성해주어야 한다.
-RoomDatabase의 각 단일 테이블'행'은 고유의 키 값을 가지고 있어야지 DAO에서 접근이 가능하기 때문에 키 값을 저장할 수 있는 변수를 꼭 작성해주어야 하고, 데이터베이스의 테이블에 접근 할 때 해당 id를 가진 테이블이 단 한개만 존재해야 하기 때문에 PrimaryKey Annotation을 작성해서 Room에게 id 변수가 고유 접근 키값이라고 알려주어야 한다.
-또한 데이터베이스에 테이블을 추가 할 때마다 일일이 id 값을 업데이트 해주기 불편하므로 "autoGenerate = true"로 설정해주어야 한다. PrimaryKey(autoGenerate = true)는 데이터 테이블'행'이 추가 될 때마다 자동으로 1씩 키값이 증가하도록 도와준다.
-아래는 Database Item의 예시코드이다.
> 아래의 data class Contact를 RoomDatabase 내부의 테이블 행으로 사용하겠다는 것을 Room에게
알려주려면 @Entity Annotation을 사용해야 한다.
>아래의 data class Contact의 각 속성들은 RoomDatabase의 Column에 매칭된다.
@Entity
data class Contact(
val firstName: String,
val lastName: String,
val phoneNumber: String,
>데이터베이스의 각 단일 테이블에는 고유한 접근 키 값을 가지고 있어야 하기 때문에 data class에
id를 넣어준것이다.
>데이터베이스의 테이블에 접근 할 때 해당 id를 가진 테이블이 단 한개만 존재해야 하기 때문에
PrimaryKey Annotation을 작성해서 Room에게 id 변수가 고유 접근 키값이라고 알려주어야 한다.
>또한 데이터베이스에 테이블을 추가 할 때마다 일일이 id 값을 업데이트 해주기 불편하므로
"autoGenerate = true"로 설정해주어야 한다.
>PrimaryKey(autoGenerate = true)는 데이터 테이블'행'이 추가 될 때마다 자동으로 1씩 키값이
증가하도록 도와준다.
@PrimaryKey(autoGenerate = true)
val id: Int = 0
)
-DAO는 RoomDatabase에 저장된 데이터를 처리하는 방법을 정의한 인터페이스이다. 이렇게 인터페이스를 사용해서 정의하였기 때문에 클린아키텍처의 원칙인 관심사의 분리를 유지할 수 있다.
-RoomDatabase에 접근해서 데이터를 관리하고 싶을 때 필요한 메서드를 정의하고 싶으면 Dao Annotation을 작성해주면 된다. Dao Annotation을 사용하게 되면 Room이 Dao Annotation을 적용한 인터페이스를 구현한 데이터 엑세스 객체를 자동으로 생성해서 해당 interface를 활용해서 데이터를 관리 및 처리 할 수 있다.
-@Dao Interface 내부에 데이터를 처리 및 관리 할 수 있는 메서드를 정의 할 때 parameter에 Entity Annotation을 달아준 데이터 항목을 넣어주어야 한다.
-Dao Interface에 들어가는 메서드는 두가지의 유형이 있다.
// RoomDatabase에 새로운 테이블'행'을 추가한다.
@Insert
suspend fun InsertContact(contact: Contact)
// RoomDatabase에서 테이블'행'을 삭제한다.
@Delete
suspend fun deleteContact(contact: Contact)
// RoomDatabase에 기존 테이블'행'을 업데이트한다.
@Update
suspend fun UpdateContact(contact: Contact)
// RoomDatabase에 새로운 테이블'행'을 추가하거나 이미 존대한다면 , 테이블'행'을 업데이트한다
@Upsert
suspend fun upsertContact(contact: Contact)
// SELECT * : 다 선택해라
// FROM contact : contact Table에서
// ORDER BY firstName ASC : firstName 열의 값을 오름차순 정렬해라
// ORDER BY lastName ASC : lastName 열의 값을 오름차순 정렬해라
// ORDER BY phoneNumber ASC : phoneNumber 열의 값을 오름차순 정렬해라
// ORDER BY AAA DESC : AAA 열의 값을 내림차순 정렬해라
@Query("SELECT * FROM contact ORDER BY firstName ASC")
fun getContactsOrderedByFirstName(): Flow<List<Contact>>
@Query("SELECT * FROM contact ORDER BY lastName ASC")
fun getContactsOrderedByLastName(): Flow<List<Contact>>
@Query("SELECT * FROM contact ORDER BY phoneNumber ASC")
fun getContactsOrderedByPhoneNumber(): Flow<List<Contact>>
-위의 기본 개념을 기반으로 작성한 전체적인 DAO에 대한 예시코드
> RoomDatabase에 접근해서 데이터를 관리하고 싶을 때 필요한 메서드를 정의하고 싶으면
Dao Annotation을 작성해주면 되고, Dao Annotation을 사용하게 되면 Room이 Dao Annotation을
적용한 인터페이스를 구현한 데이터 엑세스 객체를 자동으로 생성해서 해당 interface를 활용해서
데이터를 관리 및 처리 할 수 있다.
@Dao
interface ContactDao {
>@Dao Interface 내부에 데이터를 처리 및 관리 할 수 있는 메서드를 정의 할 때 parameter에
Entity Annotation을 달아준 데이터 항목을 넣어주어야 한다.
// RoomDatabase에 데이터를 Update + Insert
@Upsert
suspend fun upsertContact(contact: Contact)
// RoomDatabase에 데이터를 삭제
@Delete
suspend fun deleteContact(contact: Contact)
@Query("SELECT * FROM contact ORDER BY firstName ASC")
fun getContactsOrderedByFirstName(): Flow<List<Contact>>
@Query("SELECT * FROM contact ORDER BY lastName ASC")
fun getContactsOrderedByLastName(): Flow<List<Contact>>
@Query("SELECT * FROM contact ORDER BY phoneNumber ASC")
fun getContactsOrderedByPhoneNumber(): Flow<List<Contact>>
}
-RoomDatabase의 구성요소 3단계를 만들었다면, Room.databaseBuilder()를 활용하여 실제 RoomDatabase 객체를 생성해야 한다. RoomDatabase 객체는 단 하나의 객체만 존재해야 하기 때문에 lazy{} 코들 블록 내부에서 생성해줘야 한다. lazy{} 블록을 사용하면 단 한번만 객체를 생성하고 그 뒤로는 생성한 객체를 재사용하기 때문이다.
>lazy를 사용하여 한 번만 객체를 만들고 그 뒤로는 재사용 하도록 하였다.
private val db by lazy {
>Room.databaseBuilder()는 RoomDatabase Instance를 생성하는 빌더 함수이다.
Room.databaseBuilder(
>RoomDatabase가 applicationContext에서 작동한다는 것을 알려준다.
applicationContext,
>RoomDatabase Instance를 생성할 때 사용될 Database Class를 알려준다.
ContactDatabase::class.java,
>RoomDatabase Instance의 이름을 알려준다.
"contacts.db"
).build()
>Room.databaseBuilder()의 parameter에 설정된 옵션들을 사용하여 RoomDatabase Instance 생성한다.
}
-Room.databaseBuilder(context , klass , name)에 알맞는 정보를 넣어준 후 build() 메서드를 호출하면 해당 정보를 사용해서 RoomDatabase Instance를 반환해준다.
-
-위의 gif는 실제로 앱상에서 Room 을 구현한 예시이다.
-현재 코드상에는 아래와 같이 이벤트를 모아놓은 sealed interface가 존재함을 참고바람.
sealed interface ContactEvent {
object SaveContact: ContactEvent
data class SetFirstName(val firstName: String): ContactEvent
data class SetLastName(val lastName: String): ContactEvent
data class SetPhoneNumber(val phoneNumber: String): ContactEvent
object ShowDialog: ContactEvent
object HideDialog: ContactEvent
data class SortContacts(val sortType: SortType): ContactEvent
data class DeleteContact(val contact: Contact): ContactEvent
}
-아래의 코드는 현재 코드의 ViewModel에서 발췌한 코드이며 UPSERT를 실행하는 과정을 설명하는데 필요한 코드이므로 참고바람.
private val _sortType = MutableStateFlow(SortType.FIRST_NAME.string)
private val _contacts = _sortType
.flatMapLatest { sortType ->
when(sortType) {
SortType.FIRST_NAME.string -> dao.getContactsOrderedByFirstName()
SortType.LAST_NAME.string -> dao.getContactsOrderedByLastName()
SortType.PHONE_NUMBER.string -> dao.getContactsOrderedByPhoneNumber()
else -> throw IllegalArgumentException("Unknown SortType")
}
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
private val _state = MutableStateFlow(ContactState())
val state = combine(_state, _sortType, _contacts) { state, sortType, contacts ->
state.copy(
contacts = contacts,
sortType = sortType
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ContactState())
-현재 코드에서 실제로 RoomDatabase에 UPSERT를 가능하게 하는 코드는 다음과 같다.
-즉, DAO를 활용하여 RoomDatabase의 Table을 다루는 작업은 단지 Dao 객체를 생성한 후 인터페이스 내부의 메서드를 호출하기만 하면 RoomLibrary에서 능동적으로 데이터테이블 관리를 진행해준다.
ContactEvent.SaveContact -> {
val firstName = state.value.firstName
val lastName = state.value.lastName
val phoneNumber = state.value.phoneNumber
if(firstName.isBlank() || lastName.isBlank() || phoneNumber.isBlank()) {
return
}
val contact = Contact(
firstName = firstName,
lastName = lastName,
phoneNumber = phoneNumber
)
viewModelScope.launch {
dao.upsertContact(contact)
}
_state.update { it.copy(
isAddingContact = false,
firstName = "",
lastName = "",
phoneNumber = ""
) }
}
-앱에서 어떤 클릭이벤트가 발생하거나 특정 로직이 충족 되었을 때 등등의 후행 람다식 내부에서 Dao 객체를 사용해서 RoomDatabase의 DataTable에 접근 및 관리를 활 수 있다.