SQLite는 관계형 데이터베이스이므로 엔티티 사이의 관계를 정의할 수 있음.
대부분의 객체 관계 맵핑 라이브러리는 엔티티 객체가 서로를 참조하는 것을 허용하지만 Room은 명시적으로 이것을 금지함.
@Dao
interface UserBookDao {
@Query(
"SELECT user.name AS userName, book.name AS bookName " +
"FROM user, book " +
"WHERE user.id = book.user_id"
)
fun loadUserAndBookNames(): LiveData<List<UserBook>>
}
data class UserBook(val userName: String?, val bookName: String?)
@Query(
"SELECT * FROM user" +
"JOIN book ON user.id = book.user_id"
)
fun loadUserAndBookNames(): Map<User, List<Book>>
Intermediate data class를 반드시 사용해야 할 이유가 없는 경우 Multimap return types를 권장.
객체가 여러개의 필드를 포함하더라도 데이터베이스 로직에서 객체 전체를 하나로 표현하고 싶을 경우 @Embedded
어노테이션을 사용하여 테이블 내의 하위 필드로 나타낼 수 있음.
data class Address(
val street: String?,
val state: String?,
val city: String?,
@ColumnInfo(name = "post_code") val postCode: Int
)
@Entity
data class User(
@PrimaryKey val id: Int,
val firstName: String?,
@Embedded val address: Address?
)
1:1 관계를 정의하기 위해 두 엔티티 각각에 대한 클래스를 만들고 그 중 하나의 엔티티는 다른 엔티티의 기본 키에 대한 참조 변수를 포함해야 함.
@Entity
data class User(
@PrimaryKey val userId: Long,
val name: String,
val age: Int
)
@Entity
data class Library(
@PrimaryKey val libraryId: Long,
val userOwnerId: Long
)
위의 코드에서 사용자 리스트와 Library를 쿼리하려면 먼저 두 엔티티 간의 일대일 관계를 모델링해야 함.
각 인스턴스가 부모 엔티티의 인스턴스와 해당하는 자식 엔티티의 인스턴스를 보유하는 새 데이터 클래스를 만듦.
자식 엔티티의 인스턴스에 @Relation
어노테이션을 추가.
parentColumn을 부모 엔티티의 기본 키 Column으로 설정하고 entityColumn을 부모 엔티티의 기본 키를 참조하는 자식 엔티티의 열 이름으로 설정.
data class UserAndLibrary(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userOwnerId"
)
val library: Library
)
@Transaction
주석을 추가.@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
1:N 관계를 정의하기 위해 두 엔티티에 대한 클래스를 만들어야 함. 1:1 관계와 마찬가지로 자식 엔티티는 부모 엔티티의 기본 키에 대한 참조 변수를 포함해야 함.
@Entity
data class User(
@PrimaryKey val userId: Long,
val name: String,
val age: Int
)
@Entity
data class Playlist(
@PrimaryKey val playlistId: Long,
val userCreatorId: Long,
val playlistName: String
)
User와 Playlist를 쿼리하려면 두 엔티티 간의 1:N 관계를 모델링해야 함.
각 인스턴스가 부모 엔티티의 인스턴스와 해당 자식 엔티티 인스턴스 목록을 보유하는 새 데이터클래스를 만듦.
자식 엔티티의 인스턴스에 @Relation
어노테이션을 추가.
parentColumn을 부모 엔티티의 기본 키 Column으로 설정하고 entityColumn을 부모 엔티티의 기본 키를 참조하는 자식 엔티티의 열 이름으로 설정.
data class UserWithPlaylists(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<Playlist>
)
@Transaction
주석을 추가.@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylists(): List<UserWithPlaylists>
다대다 관계를 정의하려면 두 엔티티 각각에 대한 클래스를 만듦.
다대다 관계는 일반적으로 자식 엔티티에서 부모 엔티티를 참조하는 참조가 없음.
대신 두 엔티티 간 연결 엔티티 또는 교차 참조 테이블을 나타내는 세 번째 클래스를 만듦.
교차 참조 테이블은 다대다 관계에 표시된 두 엔티티 각각의 기본키를 열로 가져야 함.
@Entity
data class Playlist(
@PrimaryKey val playlistId: Long,
val playlistName: String
)
@Entity
data class Song(
@PrimaryKey val songId: Long,
val songName: String,
val artist: String
)
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
val playlistId: Long,
val songId: Long
)
재생 목록 및 재생 목록에 해당하는 노래 목록을 쿼리
👉 하나의 Playlist 객체와 여기에 포함된 모든 노래 객체 목록을 포함하는 새로운 데이터 클래스를 만듦.
노래 및 노래에 해당하는 Playlist 목록을 쿼리
👉 하나의 Song 객체와 이 객체가 포함된 모든 Playlist 객체의 목록을 포함하는 새로운 데이터 클래스를 만듦.
data class PlaylistWithSongs(
@Embedded val playlist: Playlist,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class) //관계를 정의한 엔티티를 지정해 주어야 함.
)
val songs: List<Song>
)
data class SongWithPlaylists(
@Embedded val song: Song,
@Relation(
parentColumn = "songId",
entityColumn = "playlistId",
associateBy = Junction(PlaylistSongCrossRef::class) //관계를 정의한 엔티티를 지정해 주어야 함.
)
val playlists: List<Playlist>
)
@Transaction
@Query("SELECT * FROM Playlist")
fun getPlaylistsWithSongs(): List<PlaylistWithSongs>
@Transaction
@Query("SELECT * FROM Song")
fun getSongsWithPlaylists(): List<SongWithPlaylists>
세 개 이상의 테이블을 쿼리해야할 경우
@Entity
data class User(
@PrimaryKey val userId: Long,
val name: String,
val age: Int
)
@Entity
data class Playlist(
@PrimaryKey val playlistId: Long,
val userCreatorId: Long,
val playlistName: String
)
@Entity
data class Song(
@PrimaryKey val songId: Long,
val songName: String,
val artist: String
)
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
val playlistId: Long,
val songId: Long
)
@Relation
어노테이션을 사용하여 모델링.data class PlaylistWithSongs(
@Embedded val playlist: Playlist,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class)
)
val songs: List<Song>
)
data class UserWithPlaylistsAndSongs(
@Embedded val user: User
@Relation(
entity = Playlist::class,
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<PlaylistWithSongs>
)
@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylistsAndSongs(): List<UserWithPlaylistsAndSongs>
Room은 기본 타입(primitive)과 boxed type 간의 변환 기능 제공
앱에서 사용자 정의 데이터 유형을 데이터베이스 column에 저장해야 할 경우,
Room에게 사용자 정의 데이터를 Room이 저장할 수 있는 유형으로 변환한다는 것을 알려주는 메서드인 타입 컨버터를 제공해야 함.
@TypeConverter
어노테이션을 이용하여 식별.
Room 데이터베이스에 Date 인스턴스를 저장해야 한다고 가정하면 Room은 Date 객체를 저장시키는 방법을 알지 못하므로 타입 컨버터를 정의해야 함.
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time?.toLong()
}
}
@TypeConverters
어노테이션을 추가하여 Room이 정의한 컨버터 클래스를 인식하도록 함.@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
@Entity
data class User(private val birthday: Date?)
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE birthday = :targetDate")
fun findUsersBornOnDate(targetDate: Date): List<User>
}
위의 예시에서는 AppDatabase에 @TypeConverters
로 어노테이션 처리를 했기 때문에 정의된 타입 컨버터를 어디서든 사용 가능.
@Entity
또는 @Dao
클래스에 @TypeConverters
로 어노테이션 처리를 하면 특정 엔티티 또는 DAO에 타입 컨버터를 제한할 수 있음.
Room은 타입 컨버터를 인스턴스화하는 작업을 하지만, 때로는 타입 컨버터 클래스에 추가 의존성을 전달해야 하는 경우가 있으므로 타입 컨버터 클래스의 초기화를 직접 제어해야 할 수도 있음.
타입 컨버터 클래스에 @ProvidedTypeConverter
어노테이션을 달아줌.
@ProvidedTypeConverter
class ExampleConverter {
@TypeConverter
fun StringToExample(string: String?): ExampleType? {
...
}
@TypeConverter
fun ExampleToString(example: ExampleType?): String? {
...
}
}
그 다음, RoomDatabase.Builder.addTypeConverter()
메서드를 사용하여 컨버터 클래스의 인스턴스를 RoomDatabase 빌더에 전달.
val db = Room.databaseBuilder(...)
.addTypeConverter(exampleConverterInstance)
.build()
⭐️ Room은 엔티티 클래스 간의 객체 참조를 허용하지 않음. 대신, 앱에서 필요한 데이터를 명시적으로 요청해야 함.
데이터베이스 객체 모델로의 관계 매핑은 일반적이며 서버 측에서는 잘 작동함.
프로그램이 필드에 액세스하여 로드를 할 때도 서버는 잘 동작함.
하지만 클라이언트 측에서 이런 로딩 지연은 주로 UI 스레드에서 일어나므로 알맞은 방식이 아니며, UI 스레드에서 디스크 정보를 쿼리하는 것은 성능 문제가 발생함.
UI 스레드는 일반적으로 액티비티의 업데이트된 레이아웃을 계산하고 그리는데 약 16ms 정도의 시간이 걸리는데, 쿼리가 5ms 정도만 걸린다 하더라도 앱이 프레임을 그리는 시간이 부족해져 시각적으로 눈에 띄는 결함이 발생하게 됨. 병렬로 실행중인 별도의 트랜잭션이 있는 경우 쿼리에 더 많은 시간이 걸림.
예를 들어 책 객체 목록을 로드하는 UI가 있다고 가정하면
authorNameTextView.text = book.author.name
위 코드는 Author 테이블이 메인 스레드에서 쿼리되도록 함.
Author 정보를 미리 쿼리하는 것은 해당 데이터가 더 이상 필요하지 않을 때 데이터 로드 방식을 변경하는 것을 어렵게 만듦.
UI가 더 이상 Author 정보를 표시할 필요가 없으면 앱은 더 이상 표시하지 않는 데이터를 로드하게 되어 메모리 공간을 낭비하게 됨. Author 클래스가 Books와 같은 다른 테이블을 참조하는 경우 효율은 더 떨어짐.
💡 요약: UI 스레드에서 데이터베이스에 접근할 경우 UI가 장시간 멈춰버릴 수 있고, 유저와의 상호작용이 중요한 모바일 환경에서는 일어나서는 안되는 일이기 때문