-
SQLite 는 relational db 이기 때문에, object 간의 관계를 정의할 수 있다.
대부분의 ORM lib 이 entity object 간에 상호 참조를 지원하지만, Room 은 명시적으로 이것을 금지한다.
금지한 기술적 이유는 아래와 같다.
Understand why Room doesn’t allow object references (중략된 번역)
-
Room 은 entity class 간 object reference 를 허용하지 않는다.
대신 명시적으로 앱이 필요에 따라 data 를 요청하도록 한다.
-
client side 에서 object reference 를 통한 lazy loading 을 실현하기 어렵다. 이 lazy loading 이 보통 UI thread 에서 발효되며, UI Thread 에서의 disk 에 대한 query 는 성능 이슈를 야기한다.
게다가 query 는 다른 transaction 이 병렬적으로 작동하고 있다면 훨씬 많은 시간이 걸릴 수 있고, 시스템에서 disk 집중된 작업을 사용할 때도 마찬가지로 엄청난 지연을 야기한다. 만약 lazy loading 을 사용하지 않는다면, 앱은 사용하려는 데이터보다 더 많은 데이터를 미리 로딩해 놓는 방법으로 이를 대체할 수 있지만, 이는 대신 더 많은 메모리를 사용한다는 단점이 있다.
-
여러개의 entity 를 Room 을 사용해서 동시접근하려면, relation 정의보다는 POJO 를 만들어서 각각의 entity 를 담는 것이 좋다. 그리고 해당 table 들을 join 하는 uqery 를 작성한다. 이 잘 구조화된 model 은 Room 의 명료한 query validation 기능과 함께 앱이 data loading 시 더 적은 resource 를 쓰도록 도와준다.
Define one-to-one relationships
-
2개의 entity 간 one-to-one relationship 은 한 parent entity 가 정확히 한개의 child entity 와 매핑되는 것을 이야기한다.
예를 들어 유저가 구매한 곡 라이브러리를 가질 수 있는 음악 앱을 생각해보자.
각 유저는 오직 1개의 라이브러리를 가질 수 있다.
따라서 이 경우 User entity 와 LIbrary entity 간의 one-to-one 관계가 성립한다.
@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
)
-
User를 연결된 라이브러리와 함께 한번에 query 하기 위해서는 두 entity 간의 one-to-one relationship 을 modeling 해야만 한다.
이를 위해서는 parent entity 와 child entity 를 연결하는 새로운 data class 를 만들어줘야 하며, @Relation annotation 으로 연결해주어야 한다.
parentColumn 에는 parent entity 의 primary key 를, entityColumn 에는 child entity 의 primary key 를 참조하는 column 을 적어준다.
data class UserAndLibrary(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userOwnerId"
)
val library: Library
)
-
마지막으로 DAO class 에 return 을 relationship 을 정의한 class 로 하여 query 를 작성해준다.
이 메소드는 2개의 query 를 돌리기 때문에 @Transaction annotation 을 적어주는 것이 좋다.
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
Define one-to-many relationships
-
2개의 entity 간 one-to-many relationship 은 parent entity 가 0개 이상의 child entity 와 연결될 수 있고, child entity 는 오직 한개의 parent entity 와 연결되는 것을 의미한다.
-
음악 앱을 예로 들었을 때, user 는 playlist 를 구성할 수 있으며, 각 user는 많은 playlist 를 구성할 수 있지만, 각 playlist 는 한 user 에게만 종속된다.
@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 들을 query 하려면 2개의 entity 에 대한 one-to-many relationship 을 정의해야 한다.
이를 위해 새로운 data class 를 만들어야 하며, @Realtion annotation 으로 관계를 설정해주어야 한다.
data class UserWithPlaylists(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<Playlist>
)
-
마지막으로 관계를 지정한 data class 를 return 하는 query 를 만들어준다.
2개의 query 를 하기 때문에 @Trasnaction 으로 명시해주어야 한다.
@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylists(): List<UserWithPlaylists>
<과거 버전 글>
-
직접적으로 relationship 을 지정할 수는 없지만, Room 은 Foreign key constraints 를 entity 간 지정하도록 하는 기능은 남겨두었다.
예를 들어 Book 이라는 entity 가 있고, 이 녀석과 User entity 의 relation 을 주려면 @ForeignKey annotation 을 써주면 된다.
@Entity(foreignKeys = arrayOf(ForeignKey(entity = User::class, parentColumns = arrayOf("id"), childColumns = arrayOf("user_id"))
data class Book(
@PrimaryKey val bookId: Int,
val title:String?,
@ColumnInfo(name = "user_id") val userId:Int
)
한 명의 User 에게 0개 이상의 Book 이 link 될 수 있기 때문에 위의 예제는 User 와 Book 이 one-to-many 관계로 연결되어 있다.
-
Foreign key 는 매우 강력하여, 연결되는 entity 가 update 될 때 무엇을 할 지 지정할 수 있다.
예를 들어 SQLite 에게 user 가 지워지면 해당 user 를 참조한 모든 book을 삭제하라는 것을 @ForeignKey annotation 에 onDelete = CASCADE 를 지정해줌으로써 달성할 수 있다.
-
SQLite 는 @Insert(onConflict = REPLACE) 를 하나의 single operation 대신 REMOVE & REPLACE operation 으로 처리한다.
이 방법으로 foreign key constraints 에 대해 영향을 미칠 수 있다. 더 자세한 내용은 ON_CONFLICT 를 찾아보길
Define many-to-many relationships
-
Many-to-many relationship 은 parent entity 가 0개 이상의 child 와 연결될 수 있고, 마찬가지로 한개의 child 가 0개 이상의 parent entity 와 연결 될 수 있음을 이야기한다.
음악 앱에서 playlist 에는 여러 song 이 담길 수 있고, 한 개의 song 은 여러 playlist 에 담길 수 있다.
-
Many-to-many relationship 은 다른 relationship type 과 다루는 방식이 다르다.
새로운 associative entity(cross-reference table)을 만들어야 한다.
cross-reference table 은 반드시 many-to-many relationship 의 primary key 들을 primary key 로 가지고 있어야 한다.
@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 와 그에 따른 Song 들을 함께 query 하려면 혹은 Song 과 그가 속한 PlayList 들을 query 하려면 관계 지정이 필요하다.
다른 relationship 과는 다르게 @Relation 에 associateBy 가 명시되어 관계를 정의한 entity 를 지정해주어야 한다.
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>
)
-
DAO 에 새로 정의한 data class 를 return query 문을 만든다.
마찬가지로 2개의 query 를 돌리기 때문에 @Transaction 이 필요하다.
@Transaction
@Query("SELECT * FROM Playlist")
fun getPlaylistsWithSongs(): List<PlaylistWithSongs>
@Transaction
@Query("SELECT * FROM Song")
fun getSongsWithPlaylists(): List<SongWithPlaylists>
<과거 버전 글>
-
2개의 entity 간 many-to-many relationship 이 필요할 때가 있다.
다음의 예가 다대다 관계를 보여준다.
음악 스트리밍 앱이 있고, user 는 좋아하는 음악을 playlist 에 담을 수 있다.
각각의 playlist 는 여러 음악을 담을 수 있고, 여러 음악은 여러 playlist 에 담길 수 있다.
위 관계를 model 하기 위해서는 3개의 object 를 만들어야 한다.
1. playlist entity
2. song entity
3. song 과 playlist 사이의 정보를 담는 중간자 class
@Entity
data class Playlist(
@PrimaryKey var id:Int,
val name:String?,
val description:String?
)
@Entity
data class Song(
@PrimaryKey var id:Int,
val songName:String?,
val artistName:String?
)
@Entity(tableName = "playlist_song_join",
primaryKeys = arrayOf("playlistId", "songId"),
foreignKeys = arrayOf(
ForeignKey(entity = Playlist::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("playlistId")),
ForeignKeys(entity = Song::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("songId"))
)
data class PlaylistSongJoin(
val playlistId:Int,
val songId:Int
)
@Dao
interface PlaylistSongJoinDao{
@Insert
fun insert(playlistSongJoin:PlaylistSongJoin)
@Query("""
SELECT * FROM playlist INNER JOIN playlist_song_join
ON playlist.id=playlist_song_join.playlistId
WHERE playlist_song_join.songId=:songId
""")
fun getPlaylistForSong(songId:Int): Array<Playlist>
@Query("""
SELECT * FROM song INNER JOIN playlist_song_join
ON song.id=playlist_song_join.songId
WHERE playlist_song_join.playlistId=:playlistId
""")
fun getSongsForPlaylist(playlistId: Int): Array<Song>
}
Create nested objects
-
가끔 서로 연관있는 3~4개의 table 을 연결하여 query 할 경우가 있다.
이 경우 nested relationship 에 대한 정의가 필요하다.
예를 들어 음악 앱에서 User와 그에 귀속된 PlayList, 그리고 PlayList 안의 Song 까지 한번에 query 하고 싶은 경우이다.
-
User 는 PlayList 와 one-to-many relationship 이고, PlayList 와 Song 은 many-to-many relationship 이다.
@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
)
-
관계는 순차적으로 정의해주면 된다.
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>
)
-
마지막으로 UserWithPlayListsAndSongs 를 return 하는 query 문을 @Transaction 과 함께 정의해주면 된다.
@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylistsAndSongs(): List<UserWithPlaylistsAndSongs>
과거 버전 글
@Embedded annotation 을 이용해서 subfields 안에 있는 내용을 table 에 decompose 해서 표현할 수 있다. 그리고 해당 column 들만을 query 해서 해당 model 에 담아낼 수도 있다. (아래 예제를 참조하자)
예를 들어 User class 는 Address class 를 담고 있고, 이 class는 street, city, state, postCode 를 담고 있다.
이 address class 의 내용물을 table 에 함께 저장시키려면, @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?
)
User table 은 id, firstName, street, state, city, post_code 를 가진다.
-
embedded field 는 또 다른 embedded field 를 가질 수 있다.
-
entity 가 여러 개의 같은 타입의 embedded field 를 가지고 있다면, 해당 column 들의 uniqueness 는 prefix property 를 이용해서 보장할 수 있다. Room 에서는 prefix value 를 참조하여 각각의 embedded object 의 column name 앞에 붙여준다.
-
돼왕 주
Coordinates 가 latitude 와 longitude 를 가지고 있다면, 아래와 같은 코드는 foo_latitude 와 foo_longitude 를 만든다.
@Embedded(prefix = "foo_")
Coordinates coordinates;
-
참고자료
https://developer.android.com/training/data-storage/room/relationships
'프로그래밍 놀이터 > 안드로이드, Java' 카테고리의 다른 글
[android] Migrating Room databases - Room 에 대해 알아보자 (0) | 2021.05.03 |
---|---|
[android] Create views into a database - Room 에 대해 알아보자 (0) | 2021.05.02 |
[android] FileProvider 에 알아보자 (0) | 2021.02.16 |
[android] WebChromeClient 의 file upload (1) | 2021.01.29 |
[android] 국제화 text style 입히기 (0) | 2021.01.27 |
댓글