이 글을 쓰는 지금, 첫 토이 프로젝트로 여행 플래너를 개발하는 중이다. 플래너를 만들기 위해 Database를 다음과 같이 설계해보았다.
처음 DB를 설계해봤기에 엉망인 점이 많겠지만 원하는 대로 설계가 되었다. 이제 설계한 DB를 Room으로 구현을 해야한다. 각각의 테이블의 엔티티를 정의하고, 엔티티 간의 관계를 정의해서 데이터의 목록을 가져와야 하는데 Room에서는 어떤 방법을 사용하여 가져와야 하는지 여러 방법을 찾아보았다.
설계한 DB를 엔티티로 간단하게 정의 해보았다.
// 해시태그 테이블을 엔티티로 정의
@Entity(tableName = "hashtag")
data class Hashtag(
...
)
// 플래너 테이블을 엔티티로 정의
@Entity(tableName = "planner")
data class Planner(
...
)
// 데일리 플랜 테이블을 엔티티로 정의
@Entity(tableName = "daily_plan")
data class DailyPlan(
...
)
// 일정 테이블을 엔티티로 정의
@Entity(tableName = "plan")
data class Plan(
...
)
이제 정의한 엔티티들 간의 관계를 정의해야한다. 이때까지 찾아본 방법은 3가지가 있었다.
@Relation
을 사용하여 엔티티간 관계를 모델링하는 클래스 정의각 방법의 사용법과 장단점을 알아보자.
@TypeConverter
어노테이션은 SQLite에서 지원하지 않는 참조형 타입을 지원하는 타입으로 변환해준다.
이 방법을 사용하기 위해선 엔티티 데이터 클래스 내에 다른 엔티티 목록을 선언 해주어야 한다.
// 데일리 플랜 엔티티 내부에 일정 리스트를 선언했다.
@Entity
data class DailyPlan(
...,
var planList: List<Plan>? = null
)
List 타입을 DB에 저장하기 위해선 @TypeConverter
를 통해 String 타입인 Json 형태 로 바꿔주어야 한다. 만약 변환하지 않고 그대로 실행하면 해당 컴파일 에러가 뜨게 된다.
error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.
class ListConverter {
@TypeConverter
fun listToJson(list: List<Plan>?) = Gson.toJson(list)
@TypeConverter
fun jsonToList(value: String) = Gson().fromJson(value, Array<Plan>::class.java).toList()
}
// TypeConverter 를 정의한 클래스를 데이터베이스에 적용하기
@Database(
entities = [Hashtag::class, Planner::class, DailyPlan::class, Plan::class],
version = 1
)
@TypeConverters(ListConverter::class)
abstract class PlannerDatabase: RoomDatabase() {
...
}
Gson 라이브러리를 사용해 List -> Json, Json -> List 변환을 할 수 있게 되었다.
이러한 장단점이 있긴 하지만 이 방법은 사용하지 않는 것이 좋다고 생각한다. Room 에서는 객체 간 직접적인 참조를 금하기 때문이다.
대부분의 객체 관계 매핑(ORM) 라이브러리에서는 항목 객체가 서로를 참조할 수 있지만, Room은 이러한 상호 참조를 명시적으로 금지합니다.
그렇기 때문에 나머지 두 방법을 사용하는 것이 좋을 것 같다.
@Relation
어노테이션은 두 엔티티 간의 관계를 모델링하는 클래스를 정의하는 기능을 제공해준다.
아래의 코드는 1 : N 관계를 모델링한 클래스다.
data class DailyWithPlanList(
@Embedded val dailyPlan: DailyPlan,
@Relation(
parentColumn = "dailyId", // 상위 엔티티의 PK
entityColumn = "dailyId" // 상위 엔티티를 기본키를 참조하는 하위 엔티티 PK
)
val planList: List<Plan>
)
관계를 모델링한 클래스를 미리 정의해뒀기 때문에 테이블 끼리 JOIN 할 필요없이 반환 타입만 모델링한 클래스로 두고, 상위 엔티티를 검색하는 쿼리문을 작성하면 된다.
@Transction
@Query("SELECT * FROM daily_plan WHERE dailyId = :id AND day = :day")
fun getDailyWithPlanList(id: Long, day: Int): DailyWithPlanList
Multimap 은 Map 과 유사하지만 키가 중복될 수 있다는 차이점이 있다. 멀티매핑 반환은 멀티맵을 반환하는 방법이며, 추가로 데이터 클래스를 정의할 필요가 없다.
@Query(
"SELECT * FROM daily_plan AS daily" +
"INNER JOIN plan ON daily.dailyId = plan.dailyId"
)
fun getDailyAndPlanList(): Map<DailyPlan, List<Plan>>
위 코드에서 DailyPlan 필요없이 특정 컬럼인 day 를 가져오고 싶다. 그러기 위해선 @MapInfo
어노테이션을 사용하면 된다.
@MapInfo(keyColumn = "day")
@Query(
"SELECT * FROM daily_plan AS daily" +
"INNER JOIN plan ON daily.dailyId = plan.dailyId"
)
fun getSpecificDayAndPlanList(): Map<Int, List<Plan>>
멀티매핑 반환 방법은 Room 2.4.0 부터 지원한다.
사용 용도에 따라 @Relation, 멀티매핑 반환 을 선택해서 사용하면 된다.
@Relation 어노테이션을 사용할 때 1 : N 관계 말고도 다른 관계에 대한 내용은 Room Database 문서 에서 공부할 수 있다.