본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[android] Define relationships between objects - Room 에 대해 알아보자

by 돼지왕 왕돼지 2021. 5. 1.
반응형

 

-

SQLite 는 relational db 이기 때문에, object 간의 관계를 정의할 수 있다.

대부분의 ORM lib 이 entity object 간에 상호 참조를 지원하지만, Room 은 명시적으로 이것을 금지한다.

금지한 기술적 이유는 아래와 같다.

 

 

 

Understand why Room doesn’t allow object references (중략된 번역)

 

https://developer.android.com/training/data-storage/room/referencing-data.html#understand-no-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

 

 

 

반응형

댓글