관계형 데이터베이스는 데이터를 테이블과 열, 행으로 구성하는 일반적인 데이터베이스 유형이다.
위 그림처럼 테이블간의 관계를 표현할 수 있어서 관계형 데이터베이스라고 한다.
관계형 데이터베이스에 액세스할 때는 SQL이 필요하다. SQL은 구조화된 쿼리 언어를 의미하며 이를 통해 관계형 데이터베이스에서 데이터를 읽고 조작할 수 있다.
실습의 스켈레톤 코드는 기본 db가 설정되어있다. View > Tool Windows > Database Inspector 을 누르면 데이터베이스를 볼 수 있다.
원하는 테이블을 선택한 상태로 위쪽 돋보기붙은 버튼(Open New Query Tab)을 누르면 명령어를 입력할 수 있는 텍스트 상자가 뜬다.
명령어를 입력하고 오른쪽 하단 Run 버튼을 눌러 실행할 수 있다.
SELECT * FROM park
SELECT city FROM park
SELECT name, established, city FROM park
익숙한 select
문...
SELECT name FROM park
LIMIT 5
반환되는 행 수에 제한을 설정하려면 LIMIT
를 사용한다.
SELECT name FROM park
WHERE type = "national_park"
SELECT name FROM park
WHERE type != "recreation_area"
AND area_acres > 100000
익숙한 where
문..
SELECT COUNT(*) FROM park
SELECT SUM(park_visitors) FROM park
SELECT MAX(area_acres) FROM park
WHERE type = 'national_park'
익숙하지만 자꾸 까먹는 집계 함수..
SELECT DISTINCT type FROM park
DISTINCT
키워드를 사용하여 쿼리 결과에서 중복 값을 제거할 수 있다.
SELECT name FROM park
ORDER BY name
SELECT name FROM park
ORDER BY name DESC //내림차순
ORDER BY
절을 사용하여 SELECT
문의 결과를 정렬할 수 있다.
SELECT type, name FROM park
GROUP BY type
ORDER BY name
GROUP BY
절로 묶을 수 있다..
INSERT INTO table_name
VALUES (column1, column2, ...)
익숙한 insert
문..
UPDATE park
SET area_acres = 46,
established = 1088640000,
type = 'office'
WHERE name = 'Googleplex'
update
문...여러개를 한꺼번에 업뎃할 수 있다..
DELETE FROM park
WHERE name = 'Googleplex'
익숙한 delete
문...
Android 앱에서 데이터베이스를 쉽게 사용하는 방법은 Room
이라는 라이브러리를 사용하는 것이다. Room은 ORM(객체 관계형 매핑) 라이브러리라고 하며 이름 그대로 데이터베이스의 테이블을 Kotlin 코드에서 사용할 수 있는 객체에 매핑한다.
Room을 이용해 db를 읽어서 리사이클러뷰에 띄우는 실습을 해볼것이다.
// Entity 주석 있으면 Room이 이 클래스를 db 테이블을 정의하는 데 사용할 수 있는 것으로 인식한다.
@Entity
data class Schedule(
@PrimaryKey val id: Int,
@NonNull @ColumnInfo(name = "stop_name") val stopName: String,
@NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int
)
테이블 만들기 위한 데이터 클래스 정의. Int->Integer 같은 건 신경쓸 필요 없다. Room이 알아서 바꿔줌.
Room을 통합하기 위해 추가해야 할 다음 클래스는 DAO이다. DAO는 데이터 액세스 객체를 나타내며 데이터 액세스 권한을 제공하는 Kotlin 클래스이다. DAO에서 함수를 호출하는 것은 데이터베이스에서 SQL 명령어를 실행하는 것과 같다.
@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의 일부를 뷰 모델이라는 별도의 클래스로 분리하는 것이 좋다. 이는 모바일 앱의 일반적인 아키텍처 패턴이다. 뷰 모델을 사용하면 앱의 UI 코드와 데이터 모델을 명확하게 구분할 수 있다. 이전 코드랩에서 배웠든 ViewModel
은 수명주기도 관리하기 때문에 Dao는 데이터 로드에 집중할 수 있다.
//아까 만든 Dao 클래스를 매개변수로 사용
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {
fun fullSchedule(): List<Schedule> = scheduleDao.getAll()
fun scheduleForStopName(name: String): List<Schedule> = scheduleDao.getByStopName(name)
}
//수명 주기에 응답할 수 있는 객체를 인스턴스화
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")
}
}
AppDatabase
클래스를 사용하면 모델과 DAO 클래스, 실행하려는 모든 데이터베이스 설정을 완벽하게 제어할 수 있다.
//RoomDatabase에서 상속받는 추상 클래스 AppDatabase
@Database(entities = arrayOf(Schedule::class), version = 1)
abstract class AppDatabase: RoomDatabase() {
abstract fun scheduleDao(): ScheduleDao
//데이터베이스 객체는 한개만 있으면 됨
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
// AppDatabase 인스턴스를 반환하는 함수
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
}
}
}
}
이제 Application
클래스의 맞춤 서브클래스를 제공하면 db를 읽을 수 있다.
class BusScheduleApplication: Application() {
//getDatabase()의 결과를 보유할 lazy 속성
val database: AppDatabase by lazy { AppDatabase.getDatabase(this)}
}
ListAdapter
를 적절히 구현하고 나면 대강 어플이 만들어진다.
앱에서는 아직 동적 업데이트를 처리할 수 없다. db에 변경이 있을시, 확인하려면 앱을 종료했다 다시 실행해야 한다.
이 문제는 List<Schedule>
이 각 DAO 함수에서 한 번만 반환되기 때문에 발생한다. 이 문제를 수정하려면 DAO가 데이터베이스에서 데이터를 지속적으로 내보낼 수 있는 Flow라는 Kotlin 기능을 활용하면 된다.
Dao 클래스 먼저 수정하자.
@Dao
interface ScheduleDao {
@Query("SELECT * FROM schedule ORDER BY arrival_time ASC")
//fun getAll(): List<Schedule>
fun getAll(): Flow<List<Schedule>> //변경
@Query("SELECT * FROM schedule WHERE stop_name = :stopName ORDER BY arrival_time ASC")
//fun getByStopName(stopName: String): List<Schedule>
fun getByStopName(stopName: String): Flow<List<Schedule>> //변경
}
DAO에 액세스하는 뷰 모델의 함수도 업데이트한다.
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {
//아래 두 줄 변경
fun fullSchedule(): Flow<List<Schedule>> = scheduleDao.getAll()
fun scheduleForStopName(name: String): Flow<List<Schedule>> = scheduleDao.getByStopName(name)
}
이제 실시간으로 db가 변하면 ui도 업뎃된다.