객체 간 관계 정의

SQLite는 관계형 데이터베이스이므로 항목 간 관계를 정의할 수 있습니다. 대부분의 객체 관계 매핑(ORM) 라이브러리에서는 항목 객체가 서로를 참조할 수 있지만, Room은 이러한 상호 참조를 명시적으로 금지합니다. 이 결정의 기술적 근거를 알아보려면 Room에서 객체 참조를 허용하지 않는 이유를 참고하세요.

가능한 접근 방식 2가지

Room에서는 2가지 방법으로 항목 간 관계를 정의하고 쿼리합니다. 즉, 삽입된 객체가 있는 중간 데이터 클래스나 멀티매핑 반환 유형이 있는 관계형 쿼리 메서드를 사용할 수 있습니다.

중간 데이터 클래스

중간 데이터 클래스 접근 방식에서는 Room 항목 간 관계를 모델링하는 데이터 클래스를 정의합니다. 이 데이터 클래스는 한 항목의 인스턴스와 다른 항목의 인스턴스로 구성된 쌍을 삽입된 객체로 보유합니다. 그러면 쿼리 메서드가 앱에서 사용할 이 데이터 클래스의 인스턴스를 반환할 수 있습니다.

예를 들어 UserBook 데이터 클래스를 정의하여 특정 책을 대출한 도서관 이용자를 표시하고, 쿼리 메서드를 정의하여 데이터베이스에서 UserBook 인스턴스 목록을 검색할 수 있습니다.

Kotlin

@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?)

Java

@Dao
public interface UserBookDao {
   @Query("SELECT user.name AS userName, book.name AS bookName " +
          "FROM user, book " +
          "WHERE user.id = book.user_id")
   public LiveData<List<UserBook>> loadUserAndBookNames();
}

public class UserBook {
    public String userName;
    public String bookName;
}

멀티매핑 반환 유형

멀티매핑 반환 유형 접근 방식에서는 데이터 클래스를 추가로 정의하지 않아도 됩니다. 대신 원하는 매핑 구조에 기반하여 메서드의 멀티매핑 반환 유형을 정의하고 SQL 쿼리에서 항목 간 관계를 직접 정의합니다.

예를 들어 다음 쿼리 메서드는 UserBook 인스턴스의 매핑을 반환하여 특정 책을 대출한 도서관 이용자를 나타냅니다.

Kotlin

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id"
)
fun loadUserAndBookNames(): Map<User, List<Book>>

Java

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id"
)
public Map<User, List<Book>> loadUserAndBookNames();

접근 방식 선택

Room은 두 접근 방식을 모두 지원하므로 내 앱에 가장 적합한 접근 방식을 사용할 수 있습니다. 이 섹션에서는 개발자에 따라 특정 접근 방식이 더 유용할 수 있는 이유를 설명합니다.

중간 데이터 클래스 접근 방식을 사용하면 복잡한 SQL 쿼리를 작성하지 않아도 되지만, 추가 데이터 클래스가 필요하므로 코드의 복잡성이 증가할 수도 있습니다. 즉, 멀티매핑 반환 유형 접근 방식에서는 SQL 쿼리로 더 많은 작업을 실행해야 하고 중간 데이터 클래스 접근 방식에서는 코드로 더 많은 작업을 해야 합니다.

중간 데이터 클래스를 사용해야 하는 것이 아니라면 멀티매핑 반환 유형 접근 방식을 사용하는 것이 좋습니다. 이 접근 방식에 관한 자세한 내용은 멀티매핑 반환을 참고하세요.

이 가이드의 나머지 부분에서는 중간 데이터 클래스 접근 방식을 사용하여 관계를 정의하는 방법을 보여줍니다.

삽입된 객체 만들기

때로 개발자는 객체에 여러 필드가 포함되어 있는 경우에도 데이터베이스 로직에서 항목 또는 데이터 객체를 응집된 전체로 표현하고자 합니다. 이 경우 @Embedded 주석을 사용하여 테이블 내의 하위 필드로 분해하려고 하는 객체를 나타낼 수 있습니다. 그러면 다른 개별 열을 쿼리하듯 삽입된 필드를 쿼리할 수 있습니다.

예를 들어 User 클래스는 Address 유형의 필드를 포함할 수 있으며 이것은 street, city, state, postCode 필드로 구성된 유형임을 나타냅니다. 구성된 열을 테이블에 별도로 저장하려면 다음 코드 스니펫에서와 같이 @Embedded로 주석 처리된 Address 필드를 User 클래스에 포함하세요.

Kotlin

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?
)

Java

public class Address {
    public String street;
    public String state;
    public String city;

    @ColumnInfo(name = "post_code") public int postCode;
}

@Entity
public class User {
    @PrimaryKey public int id;

    public String firstName;

    @Embedded public Address address;
}

User 객체를 나타내는 테이블에는 id, firstName, street, state, city, post_code라는 이름의 열이 포함됩니다.

항목에 동일한 유형의 삽입된 필드가 여러 개 있으면 prefix 속성을 설정하여 각 열을 고유하게 유지할 수 있습니다. 그러면 Room은 삽입된 객체의 각 열 이름 시작 부분에 제공된 값을 추가합니다.

일대일 관계 정의

두 항목 간의 일대일 관계는 상위 항목의 각 인스턴스가 정확히 하나의 하위 항목 인스턴스에 상응하는 관계이며, 그 반대의 경우도 마찬가지입니다.

예를 들어 사용자가 소유한 노래 라이브러리가 있는 음악 스트리밍 앱을 생각해 보세요. 사용자마다 라이브러리가 한 개만 있고 각 라이브러리는 정확히 한 명의 사용자에 상응합니다. 따라서 User 항목과 Library 항목은 일대일 관계입니다.

일대일 관계를 정의하려면 먼저 두 항목 각각의 클래스를 만듭니다. 항목 중 하나는 다른 항목의 기본 키를 참조하는 변수를 포함해야 합니다.

Kotlin

@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
)

Java

@Entity
public class User {
    @PrimaryKey public long userId;
    public String name;
    public int age;
}

@Entity
public class Library {
    @PrimaryKey public long libraryId;
    public long userOwnerId;
}

사용자 및 상응하는 라이브러리의 목록을 쿼리하려면 먼저 두 항목 간의 일대일 관계를 모델링해야 합니다. 그러려면 각 인스턴스가 상위 항목 인스턴스 및 상응하는 하위 항목 인스턴스를 보유하는 새 데이터 클래스를 만듭니다. parentColumn을 상위 항목의 기본 키 열 이름으로 설정하고 entityColumn을 상위 항목의 기본 키를 참조하는 하위 항목의 열 이름으로 설정하여 @Relation 주석을 하위 항목 인스턴스에 추가합니다.

Kotlin

data class UserAndLibrary(
    @Embedded val user: User,
    @Relation(
         parentColumn = "userId",
         entityColumn = "userOwnerId"
    )
    val library: Library
)

Java

public class UserAndLibrary {
    @Embedded public User user;
    @Relation(
         parentColumn = "userId",
         entityColumn = "userOwnerId"
    )
    public Library library;
}

마지막으로 상위 항목과 하위 항목을 쌍으로 묶는 데이터 클래스의 모든 인스턴스를 반환하는 메서드를 DAO 클래스에 추가합니다. 이 메서드를 사용하려면 Room에서 쿼리 2개를 실행해야 하므로 전체 작업이 각각 실행되도록 이 메서드에 @Transaction 주석을 추가해야 합니다.

Kotlin

@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>

Java

@Transaction
@Query("SELECT * FROM User")
public List<UserAndLibrary> getUsersAndLibraries();

일대다 관계 정의

두 항목 간의 일대다 관계는 상위 항목의 각 인스턴스가 0개 이상의 하위 항목 인스턴스에 상응하지만 하위 항목의 각 인스턴스는 정확히 하나의 상위 항목 인스턴스에만 상응할 수 있는 관계입니다.

음악 스트리밍 앱의 예에서 사용자가 노래를 재생목록으로 구성할 수 있다고 가정해 보겠습니다. 각 사용자는 원하는 수만큼 재생목록을 만들 수 있지만 각 재생목록을 만든 사용자는 단 한 명입니다. 따라서 User 항목과 Playlist 항목은 일대다 관계입니다.

일대다 관계를 정의하려면 먼저 두 항목의 클래스를 만듭니다. 일대일 관계에서와 같이 하위 항목은 상위 항목의 기본 키 참조인 변수를 포함해야 합니다.

Kotlin

@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
)

Java

@Entity
public class User {
    @PrimaryKey public long userId;
    public String name;
    public int age;
}

@Entity
public class Playlist {
    @PrimaryKey public long playlistId;
    public long userCreatorId;
    public String playlistName;
}

사용자 목록 및 상응하는 재생목록을 쿼리하려면 먼저 두 항목 간의 일대다 관계를 모델링해야 합니다. 그러려면 각 인스턴스가 상위 항목 인스턴스 및 상응하는 모든 하위 항목 인스턴스 목록을 보유하는 새 데이터 클래스를 만듭니다. parentColumn을 상위 항목의 기본 키 열 이름으로 설정하고 entityColumn을 상위 항목의 기본 키를 참조하는 하위 항목의 열 이름으로 설정하여 @Relation 주석을 하위 항목 인스턴스에 추가합니다.

Kotlin

data class UserWithPlaylists(
    @Embedded val user: User,
    @Relation(
          parentColumn = "userId",
          entityColumn = "userCreatorId"
    )
    val playlists: List<Playlist>
)

Java

public class UserWithPlaylists {
    @Embedded public User user;
    @Relation(
         parentColumn = "userId",
         entityColumn = "userCreatorId"
    )
    public List<Playlist> playlists;
}

마지막으로 상위 항목과 하위 항목을 쌍으로 묶는 데이터 클래스의 모든 인스턴스를 반환하는 메서드를 DAO 클래스에 추가합니다. 이 메서드를 사용하려면 Room에서 쿼리 2개를 실행해야 하므로 전체 작업이 각각 실행되도록 이 메서드에 @Transaction 주석을 추가해야 합니다.

Kotlin

@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylists(): List<UserWithPlaylists>

Java

@Transaction
@Query("SELECT * FROM User")
public List<UserWithPlaylists> getUsersWithPlaylists();

다대다 관계 정의

두 항목 간의 다대다 관계는 상위 항목의 각 인스턴스가 0개 이상의 하위 항목 인스턴스에 상응하며, 그 반대의 경우도 마찬가지입니다.

음악 스트리밍 앱의 예에서 사용자가 정한 재생목록의 노래를 생각해 보세요. 각 재생목록에는 노래가 여러 곡 포함될 수 있으며 각 노래는 서로 다른 여러 재생목록에 속할 수 있습니다. 따라서 Playlist 항목과 Song 항목은 다대다 관계입니다.

다대다 관계를 정의하려면 먼저 두 항목 각각의 클래스를 만듭니다. 다대다 관계는 일반적으로 하위 항목에 상위 항목에 대한 참조가 없다는 점에서 다른 관계 유형과 구별됩니다. 대신 세 번째 클래스를 만들어 두 항목 간의 연결 항목(또는 상호 참조 테이블)을 나타냅니다. 상호 참조 테이블에는 테이블에 표시된 다대다 관계에 있는 각 항목의 기본 키 열이 있어야 합니다. 이 예에서 상호 참조 테이블의 각 행은 Playlist 인스턴스와 Song 인스턴스의 쌍이며, 여기서 참조된 노래는 참조된 재생목록에 포함됩니다.

Kotlin

@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
)

Java

@Entity
public class Playlist {
    @PrimaryKey public long playlistId;
    public String playlistName;
}

@Entity
public class Song {
    @PrimaryKey public long songId;
    public String songName;
    public String artist;
}

@Entity(primaryKeys = {"playlistId", "songId"})
public class PlaylistSongCrossRef {
    public long playlistId;
    public long songId;
}

다음 단계는 이러한 관련 항목을 쿼리하는 방법에 따라 다릅니다.

  • 재생목록 및 각 재생목록에 상응하는 노래 목록을 쿼리하려면 단일 Playlist 객체 및 재생목록에 포함된 모든 Song 객체 목록을 포함하는 새 데이터 클래스를 만듭니다.
  • 노래 및 각 노래에 상응하는 재생목록을 쿼리하려면 단일 Song 객체 및 해당 노래가 포함된 모든 Playlist 객체 목록을 포함하는 새 데이터 클래스를 만듭니다.

어느 경우든 이러한 각 클래스의 @Relation 주석에서 associateBy 속성을 사용하여 항목 간의 관계를 모델링함으로써 Playlist 항목과 Song 항목 간의 관계를 알려주는 상호 참조 항목을 식별합니다.

Kotlin

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>
)

Java

public class PlaylistWithSongs {
    @Embedded public Playlist playlist;
    @Relation(
         parentColumn = "playlistId",
         entityColumn = "songId",
         associateBy = @Junction(PlaylistSongCrossref.class)
    )
    public List<Song> songs;
}

public class SongWithPlaylists {
    @Embedded public Song song;
    @Relation(
         parentColumn = "songId",
         entityColumn = "playlistId",
         associateBy = @Junction(PlaylistSongCrossref.class)
    )
    public List<Playlist> playlists;
}

마지막으로 DAO 클래스에 메서드를 추가하여 앱에 필요한 쿼리 기능을 노출합니다.

  • getPlaylistsWithSongs: 이 메서드는 데이터베이스를 쿼리하고 결과 PlaylistWithSongs 객체를 모두 반환합니다.
  • getSongsWithPlaylists: 이 메서드는 데이터베이스를 쿼리하고 결과 SongWithPlaylists 객체를 모두 반환합니다.

이러한 메서드를 사용하려면 Room에서 쿼리 2개를 실행해야 하므로 전체 작업이 원자적으로 실행되도록 두 메서드에 @Transaction 주석을 추가해야 합니다.

Kotlin

@Transaction
@Query("SELECT * FROM Playlist")
fun getPlaylistsWithSongs(): List<PlaylistWithSongs>

@Transaction
@Query("SELECT * FROM Song")
fun getSongsWithPlaylists(): List<SongWithPlaylists>

Java

@Transaction
@Query("SELECT * FROM Playlist")
public List<PlaylistWithSongs> getPlaylistsWithSongs();

@Transaction
@Query("SELECT * FROM Song")
public List<SongWithPlaylists> getSongsWithPlaylists();

중첩된 관계 정의

때로 서로 관련이 있는 세 개 이상의 테이블 집합을 쿼리해야 할 수도 있습니다. 이 경우 테이블 간에 중첩된 관계를 정의합니다.

음악 스트리밍 앱의 예에서 모든 사용자, 각 사용자의 모든 재생목록, 각 사용자의 각 재생목록에 있는 모든 노래를 쿼리하려 한다고 가정해 보겠습니다. 사용자는 재생목록과 일대다 관계가 있으며 재생목록은 노래와 다대다 관계가 있습니다. 다음 코드 예에서는 이러한 항목을 나타내는 클래스뿐만 아니라 재생목록과 노래 간의 다대다 관계를 제시하는 상호 참조 테이블을 보여줍니다.

Kotlin

@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
)

Java

@Entity
public class User {
    @PrimaryKey public long userId;
    public String name;
    public int age;
}

@Entity
public class Playlist {
    @PrimaryKey public long playlistId;
    public long userCreatorId;
    public String playlistName;
}
@Entity
public class Song {
    @PrimaryKey public long songId;
    public String songName;
    public String artist;
}

@Entity(primaryKeys = {"playlistId", "songId"})
public class PlaylistSongCrossRef {
    public long playlistId;
    public long songId;
}

먼저 데이터 클래스 및 @Relation 주석을 사용하여 평소처럼 집합 내 두 테이블 간의 관계를 모델링합니다. 다음 예는 Playlist 항목 클래스와 Song 항목 클래스 간의 다대다 관계를 모델링하는 PlaylistWithSongs 클래스를 보여줍니다.

Kotlin

data class PlaylistWithSongs(
    @Embedded val playlist: Playlist,
    @Relation(
         parentColumn = "playlistId",
         entityColumn = "songId",
         associateBy = Junction(PlaylistSongCrossRef::class)
    )
    val songs: List<Song>
)

Java

public class PlaylistWithSongs {
    @Embedded public Playlist playlist;
    @Relation(
         parentColumn = "playlistId",
         entityColumn = "songId",
         associateBy = Junction(PlaylistSongCrossRef.class)
    )
    public List<Song> songs;
}

이 관계를 나타내는 데이터 클래스를 정의한 후 집합의 다른 테이블과 첫 번째 관계 클래스 간의 관계를 모델링하여 새 관계 내부에 기존 관계를 '중첩'하는 또 다른 데이터 클래스를 만듭니다. 다음 예는 User 항목 클래스와 PlaylistWithSongs 관계 클래스 간의 일대다 관계를 모델링하는 UserWithPlaylistsAndSongs 클래스를 보여줍니다.

Kotlin

data class UserWithPlaylistsAndSongs(
    @Embedded val user: User
    @Relation(
        entity = Playlist::class,
        parentColumn = "userId",
        entityColumn = "userCreatorId"
    )
    val playlists: List<PlaylistWithSongs>
)

Java

public class UserWithPlaylistsAndSongs {
    @Embedded public User user;
    @Relation(
        entity = Playlist.class,
        parentColumn = "userId",
        entityColumn = "userCreatorId"
    )
    public List<PlaylistWithSongs> playlists;
}

UserWithPlaylistsAndSongs 클래스는 세 가지의 모든 항목 클래스(User, Playlist, Song) 간의 관계를 간접적으로 모델링합니다. 이는 그림 1에 설명되어 있습니다.

UserWithPlaylistsAndSongs는 User와 PlaylistWithSongs 간의 관계를 모델링하며 이는 결과적으로 Playlist와 Song 간의 관계를 모델링합니다.

그림 1. 음악 스트리밍 앱 예에서 관계 클래스의 다이어그램

집합에 테이블이 더 많이 있다면 나머지 각 테이블 간의 관계를 모델링하는 클래스 및 이전의 모든 테이블 간의 관계를 모델링하는 관계 클래스를 만듭니다. 이렇게 하면 쿼리하려는 모든 테이블 간에 중첩된 관계 체인이 생성됩니다.

마지막으로 DAO 클래스에 메서드를 추가하여 앱에 필요한 쿼리 기능을 노출합니다. 이 메서드를 사용하려면 Room에서 여러 쿼리를 실행해야 하므로 전체 작업이 원자적으로 실행되도록 @Transaction 주석을 추가해야 합니다.

Kotlin

@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylistsAndSongs(): List<UserWithPlaylistsAndSongs>

Java

@Transaction
@Query("SELECT * FROM User")
public List<UserWithPlaylistsAndSongs> getUsersWithPlaylistsAndSongs();

추가 리소스

Room의 항목 간 관계 정의에 관한 자세한 내용은 다음 추가 리소스를 참고하세요.

샘플

동영상

블로그