관계형 데이터베이스는 데이터를 테이블과 열, 행으로 구성하는 일반적인 데이터베이스 유형이다. 관계형 데이터베이스에서 테이블은 데이터를 나타내는 것 외에도 다른 테이블을 참조하거나 테이블 간 관계를 확인 할 수 있다.
열
은 사물의 특정 속성
행
은 테이블에 저장된 실제 데이터
테이블 행의 고유 식별자 역할을 하는 것을 키본 키라고 한다. 기본 키는 다른 테이블에서 참조할 때 유용하다.
SQL(sequel)은 구조화된 쿼리 언어를 의미하며 이를 통해 관계형 데이터베이스에서 데이터를 읽고 조작할 수 있다.
가장 일반적으로 사용하는 SQL문에는 SELECT
INSERT
UPDATE
DELETE
가 있다.
안드로이드의 database inspector를 사용해 쿼리를 실행할 수 있다.
Android 스튜디오 2020.3.1 Arctic Fox를 사용하고 있다면 View > Tool > App Inspection을 통해 Database Inspector에 액세스할 수 있습니다.
// 처음 5개만 반환하도록 limit
SELECT name FROM park
LIMIT 5
// != 연산자 사용
SELECT name FROM park
WHERE type != "recreation_area"
AND area_acres > 100000
// COUNT() SUM() MAX() 등 여러 함수
SELECT MAX(area_acres) FROM park
WHERE type = 'national_park'
// GROUP BY, ORDER BY
SELECT type, name FROM park
GROUP BY type
ORDER BY name
// INSERT, UPDATE, DELETE
앱에서 Room을 사용하도록 데이터베이스 클래스를 정의하고, 데이터베이스를 채운다.
Room을 사용하려면 gradle에 다음 항목을 추가해야 한다.
build.gradle (project)
ext{
room_version = '2.3.0'
}
build.gradle (module)
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
Room에서 각 테이블은 클래스로 표시된다. Bus Schedule 앱은 Schedule
이라는 단일 테이블로 구성된다.
database.schedule
패키지 아래 Schedule.kt
파일을 생성하고 이곳에 데이터 클래스를 정의한다.
@Entity
data class Schedule(
@PrimaryKey val id: Int,
@NonNull @ColumnInfo(name = "stop_name") val stopName: String,
@NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int
)
id
는 기본 키@ColumnInfo
로 새 열의 이름을 정한다. @Entity
를 추가한다.DAO는 데이터 액세스 권한을 제공하는 Kotlin 클래스이다.
DAO에는 데이터를 읽고 조작하는 함수가 포함된다.
database.schedule
패키지 아래 ScheduleDao.kt
파일을 추가하고 인터페이스를 정의한다.
@Dao
interface ScheduleDao{
@Query("SELECT * FROM schedule ORDER BY arrival_time ASC")
fun getAll(): List<Schedule>
@Query(
"SELECT * FROM schedule WHERE stop_name = :stopName ORDER BY arrival_time ASC"
)
fun getByStopName(stopName: String): List<Schedule>
}
@Dao
를 추가해 Room에서 인터페이스를 사용할 수 있도록 한다.stopName
과 일치하는 결과만 검색하기 위해 WHERE stop_name = :stopName
이라고 작성한다. ScheduleDao
를 그대로 사용할 수 있지만 두 개 이상의 화면으로 작업할 경우 뷰에 노출하는 DAO의 일부를 별도의 ViewModel로 분리하는 것이 좋다.
viewmodels
패키지 아래 BusScheduleViewModel.kt
파일을 생성한다.
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {
fun fullSchedule(): List<Schedule> = scheduleDao.getAll()
fun scheduleForStopName(name: String): List<Schedule> = scheduleDao.getByStopName(name)
}
뷰 모델을 lifecycle 이벤트에 응답할 수 있는 객체로 인스턴스화 해야한다. 프래그먼트에서 처리하는 대신 인스턴스화에 factory 클래스를 사용할 수 있다.
뷰 모델 클래스 아래 팩토리 클래스를 추가한다.
class BusScheduleViewModelFactory( private val scheduleDao: ScheduleDao) : ViewModelProvider.Factory{
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(BusScheduleViewModel::class.java)){
@Suppress("UNCHECKED_CAST")
return BusScheduleViewModel(scheduleDao) as T
}
throw IllegalArgumentException("Unknown viewmodel class")
}
}
이제 뷰 모델이 lifecycle을 인식할 수 있다.
다음 내용을 담고있는 AppDatabase 클래스가 필요하다.
AppDatabase.kt
abstract class AppDatabase: RoomDatabase() {
abstract fun scheduleDao(): ScheduleDao
@Database(entities = arrayOf(Schedule::class), version = 1)
companion object{
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase{
return INSTANCE?: synchronized(this){
val instance = Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
)
.createFromAsset("database/bus_schedule.db")
.build()
INSTANCE = instance
instance
}
}
}
}
RoomDatabase
에서 상속받는 추상 클래스AppDatabase
클래스를 사용할 때 데이터베이스 인스턴스가 하나만 있는지 확인하기 때문에 companion object를 추가한다.@Volatile
은 잠재적 버그를 방지하기 위함?AppDatabase
인스턴스를 반환하는 함수 getDatabase
를 정의한다.BusScheduleApplication.kt
파일을 생성하고, Application
에서 상속받는 클래스를 생성한다.
class BusScheduleApplication: Application() {
val database: AppDatabase by lazy { AppDatabase.getDatabase(this) }
}
Manifest
에서 application 태그의 android:name 속성을 BusScheduleApplication으로 설정한다.
이제 모델을 뷰에 연결할 ListAdapter를 작성한다.
지금까지 배운 RecyclerView Adapter는 정적 데이터 목록을 표시했다. 이 방식은 데이터베이스를 사용할 때 데이터 변경사항을 실시간으로 처리할 수 없다. 동적으로 변경되는 목록은 ListAdapter로 처리한다.
ListAdapter는 AsyncListDiffer
를 사용하여 이전 데이터 목록과 새 데이터 목록의 차이를 확인한다. 그러면 recycler 뷰가 두 목록 간 차이에 기반해 업데이트를 한다.
ListAdapter를 확장하는 클래스를 만든다.
class BusStopAdapter(private val onItemClicked: (Schedule) -> Unit) : ListAdapter<Schedule, BusStopAdapter.BusStopViewHolder>(DiffCallback) {
}
어댑터 내부에 뷰 홀더가 있어야 레이아웃 파일에서 만들어진 뷰에 액세스할 수 있다.
BusStopViewHolder
클래스를 추가하고 bind()
함수를 구현한다.
class BusStopViewHolder(private var binding: BusStopItemBinding): RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SimpleDateFormat")
fun bind(schedule: Schedule) {
binding.stopNameTextView.text = schedule.stopName
binding.arrivalTimeTextView.text = SimpleDateFormat(
"h:mm a").format(Date(schedule.arrivalTime.toLong() * 1000)
)
}
}
재정의하고, 레이아웃 inflate하고, onItemClicked()를 호출하도록 onClickLister()를 설정한다.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BusStopViewHolder {
val viewHolder = BusStopViewHolder(
BusStopItemBinding.inflate(
LayoutInflater.from( parent.context),
parent,
false
)
)
viewHolder.itemView.setOnClickListener {
val position = viewHolder.adapterPosition
onItemClicked(getItem(position))
}
return viewHolder
}
재정의해 지정된 position에 뷰를 바인딩한다.
holder.bind(getItem(position))
ListAdapter가 목록을 업데이트할 때 새 목록과 이전 목록에서 어떤 항목이 다른지 확인하는데 필요하다.
companion object {
private val DiffCallback = object : DiffUtil.ItemCallback<Schedule>() {
override fun areItemsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
return oldItem == newItem
}
}
}
어댑터 구현이 끝났고 FullScheduleFragment.kt와 StopScheduleFragment.kt 화면에서 사용하려면 추가적인 구현이 필요하다.
빌드 시 Execution failed for task ':app:kaptDebugKotlin' 에러가 발생했다.
이 링크를 참고하니 맥북 M1에서 발생하는 문제로 kapt "org.xerial:sqlite-jdbc:3.34.0"
를 추가하니 해결되었다.
submitList()가 호출될 때마다 데이터 변경사항을 처리하고 있지만 아직 동적 업데이트를 처리할 수 었다.
⇒ List<Schedule>이 DAO 함수에서 한 번만 반환되기 때문
Flow
를 반환하도록 DAO 함수를 수정한다.
List<Schedule>
형태의 반환 유형을 Flow<List<Schedule>>
로 수정한다.