Room의 기본 요소는 다음과 같다.
Room 구성요소가 함께 작동하여 데이터베이스와 상호작용하는 걸 그림으로 나타내면 이렇다.
Entity 클래스의 각 인스턴스는 데이터베이스 테이블의 행을 나타낸다. 앱에서 Entity는 이름,가격,수량 등 Entity에 관한 정보를 보유한다.
그림을 보는게 더 이해가 잘된다.
@Entity
주석은 클래스를 데이터베이스 Entity 클래스로 표시한다.
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "name")
val itemName: String,
@ColumnInfo(name = "price")
val itemPrice: Double,
@ColumnInfo(name = "quantity")
val quantityInStock: Int
)
테이블 초기화하는 이런 코드가 Entity 클래스!
DAO 는 데이터베이스에 액세스하는 인터페이스를 정의하는 Room의 기본 구성요소이다. 여기서 만드는 DAO는 데이터베이스 쿼리/검색, 삽입, 삭제, 업데이트를 위한 편의 메서드를 제공하는 맞춤 인터페이스이다.
@Dao
interface ItemDao {
//onConflict: 충돌 발생시 Room이 뭘해야하는지 지정
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item) //정지함수로 만들어 코루틴에서 호출할 수 있게..
@Update
suspend fun update(item: Item)
@Delete
suspend fun delete(item: Item)
//삽입,업뎃,삭제는 전용 주석(@)이 존재함.
//근데 검색 등은 전용 주석 없으니까 @Query(쿼리문)의 형태로 써야 한다.
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item> //데이터 바뀌었을때 바로 대응 위해..Flow(지속성)
//윗놈은 데이터 하나 반환, 얘는 전체 반환
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
}
쿼리문 작성하는 이런 코드가 Entity 클래스!
이렇게 만든 Entity
와 DAO를 사용하는 RoomDatabase
(데이터베이스 클래스)를 정의하자. 얘는 개발자가 정의한 DAO의 인스턴스를 앱에 제공한다.
/*
@Database는 매개변수 3개가 필요함.
entities: 아까만든 item 테이블.
version: 데이터베이스 스키마 구조 바뀔때마다 +1 해야함. 초기상태니까 1
exportSchema: false로 두면 스키마 버전 기록 백업 안함.
*/
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase : RoomDatabase() {
//이놈이 dao를 알아야 지지고 볶고 하겠지..ItemDao를 반환하는 추상 함수를 선언.
abstract fun itemDao(): ItemDao
// 참고-Dao는 여러개일 수 있다.
//데이터베이스는 만드는데 비용이 많이듦. 단일 디비를 유지하자.
companion object {
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
fun getDatabase(context: Context): ItemRoomDatabase {
//db 인스턴스가 있으면 걜 반환, 없으면 synchronized 블록 실행해서 만들기
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
데이터베이스 인스턴스를 만들기 위한 붕어빵 틀이 준비가 되었으니... Application 클래스에서 데이터베이스 인스턴스를 인스턴스화한다.
class InventoryApplication : Application(){
///ItemRoomDatabase에서 getDatabase()를 호출하여 database 인스턴스를 인스턴스화
val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}
이제 Room부분은 다 준비가 됐음. ViewModel 부분을 만들어야 한다.
ViewModel
은 DAO를 통해 데이터베이스와 상호작용하여 UI에 데이터를 제공한다. 모든 데이터베이스 작업은 기본 UI 스레드에서 벗어나 실행되어야 함. 이를 위해 코루틴과 viewModelScope
를 사용하면 된다.
//ItemDao 객체를 생성자의 매개변수로!
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
//위에 있는 InventoryViewModel를 인스턴스화
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
//이상이 없으면 InventoryViewModel 인스턴스 반환
@Suppress("UNCHECKED_CAST")
return InventoryViewModel(itemDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
아직 InventoryViewModel
가 비어 있음. 이제 여기다가 원하는 함수를 만들면 된다.
//ItemDao 객체를 생성자의 매개변수로!
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {
private fun insertItem(item: Item) {
//기본 스레드 밖에서 데이터베이스와 상호작용하려면 코루틴을 시작
//ViewModelScope는 ViewModel이 소멸될 때 하위 코루틴을 자동으로 취소함.
viewModelScope.launch {
itemDao.insert(item)
}
}
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
return Item(
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
insertItem(newItem)
}
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
return false
}
return true
}
}
함수들을 추가했다! 이제 프래그먼트 등에서 가져다 쓰면 된다.
마지막으로 Database Inspector 열어서 Live update에 체크표시해주자.