SQLite はリレーショナル データベースであるため、エンティティ間のリレーションを定義できます。大部分のオブジェクト リレーショナル マッピング ライブラリにおいてはエンティティ オブジェクト間の相互参照が可能ですが、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 クエリで直接定義します。
たとえば、次のクエリメソッドは、特定の本を借りた図書館利用者を表す User
インスタンスと Book
インスタンスのマッピングを返します。
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 クエリで行う処理が多くなり、中間データクラス アプローチではコードで行う処理が多くなります。
中間データクラスを使用する特別な理由がない場合は、マルチマップの戻り値の型アプローチを使用することをおすすめします。このアプローチについて詳しくは、マルチマップを返すをご覧ください。
このガイドの残りの部分では、中間データクラス アプローチを使用してリレーションを定義する方法について説明します。
埋め込みオブジェクトを作成する
オブジェクトに複数のフィールドが含まれている場合でも、データベース ロジック内では、エンティティやデータ オブジェクトを 1 つのまとまりとして表現したい場合があります。このような場合、@Embedded
アノテーションを使用することで、テーブル内のサブフィールドに分解されるオブジェクトを表現できます。このような埋め込みフィールドは、通常の列と同様にクエリを行うことができます。
たとえば、User
クラスに Address
型のフィールドが含まれているとします。このフィールドは street
、city
、state
、postCode
という名前のフィールドで構成されています。構成後の列をテーブル内に個別に格納するには、次のコード スニペットのように、@Embedded
アノテーションを付けた User
クラスに Address
フィールドを含めます。
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
という名前の列が含まれるようになります。
1 つのエンティティに同じ型の埋め込みフィールドが複数ある場合、prefix
プロパティを設定することで、各列を一意にすることができます。指定した値が、埋め込みオブジェクトの各列名の先頭に自動的に追加されます。
1 対 1 のリレーションを定義する
2 つのエンティティ間の 1 対 1 のリレーションとは、親エンティティの各インスタンスが子エンティティの 1 つのインスタンスに対応するリレーション、あるいはその逆のリレーションです。
たとえば、音楽ストリーミング アプリで、ユーザーが自分の所有する曲のライブラリを持っている場合を考えてみましょう。各ユーザーはライブラリを 1 つだけ持ち、各ライブラリは 1 人のユーザーに対応しています。したがって、User
エンティティと Library
エンティティの間には 1 対 1 のリレーションがあります。
1 対 1 のリレーションを定義するには、まず 2 つのエンティティにそれぞれクラスを作成します。一方のエンティティには、他方のエンティティの主キーへの参照である変数を含める必要があります。
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; }
ユーザーと対応するライブラリのリストをクエリするには、最初に 2 つのエンティティ間の 1 対 1 のリレーションをモデル化する必要があります。そのために、各インスタンスが親エンティティのインスタンスと子エンティティの対応するインスタンスを保持する、新しいデータクラスを作成します。@Relation
アノテーションを子エンティティのインスタンスに追加します。このとき、parentColumn
を親エンティティの主キー列の名前に設定し、entityColumn
を親エンティティの主キーを参照する子エンティティの列の名前に設定します。
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();
1 対多のリレーションを定義する
2 つのエンティティ間の 1 対多のリレーションとは、親エンティティの各インスタンスが子エンティティの 0 個以上のインスタンスに対応し、子エンティティの各インスタンスは親エンティティの 1 つのインスタンスにだけ対応するリレーションです。
音楽ストリーミング アプリで、ユーザーがプレイリストに曲を整理できる機能があるとします。各ユーザーが作成できるプレイリストの数に制限はありませんが、各プレイリストの作成者は 1 人だけです。したがって、User
エンティティと Playlist
エンティティの間には 1 対多のリレーションがあります。
1 対多のリレーションを定義するには、まず 2 つのエンティティのクラスを作成します。1 対 1 のリレーションと同様に、子エンティティには、親エンティティの主キーへの参照である変数を含める必要があります。
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; }
ユーザーと対応するプレイリストのリストをクエリするには、最初に 2 つのエンティティ間の 1 対多のリレーションをモデル化する必要があります。そのために、各インスタンスが親エンティティのインスタンスと、対応する子エンティティのインスタンスのリストを保持する、新しいデータクラスを作成します。@Relation
アノテーションを子エンティティのインスタンスに追加します。このとき、parentColumn
を親エンティティの主キー列の名前に設定し、entityColumn
を親エンティティの主キーを参照する子エンティティの列の名前に設定します。
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();
多対多のリレーションを定義する
2 つのエンティティ間の多対多のリレーションとは、親エンティティの各インスタンスが子エンティティの 0 個以上のインスタンスに対応するリレーション、あるいはその逆のリレーションです。
音楽ストリーミング アプリの例で、ユーザー定義のプレイリストの歌について考えてみましょう。各プレイリストには多数の曲を含めることができ、それぞれの曲は多数の異なるプレイリストに含められます。したがって、Playlist
エンティティと Song
エンティティの間には多対多のリレーションがあります。
多対多のリレーションを定義するには、まず 2 つのエンティティにそれぞれクラスを作成します。多対多のリレーションは、通常、子エンティティに親エンティティへの参照がないため、他のタイプのリレーションとは区別されます。代わりに、2 つのエンティティ間の連関エンティティ(相互参照テーブルとも呼ばれます)を表す 3 つ目のクラスを作成します。相互参照テーブルには、テーブルで表現される多対多のリレーションに含まれる各エンティティからの主キーの列が必要です。この例では、相互参照テーブルの各行は、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; }
次のステップは、これらの連関エンティティのクエリ方法によって異なります。
- プレイリストと各プレイリストの対応する曲のリストをクエリする場合は、1 つの
Playlist
オブジェクトとプレイリストに含まれるSong
オブジェクトのリストを含む新しいデータクラスを作成します。 - 曲とそれに対応するプレイリストのリストをクエリする場合は、1 つの
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();
ネストされたリレーションを定義する
場合によっては、互いに関連している 3 つ以上のテーブルをクエリする必要があります。その場合、テーブル間にネストされたリレーションを定義します。
音楽ストリーミング アプリの例で、全ユーザー、各ユーザーの全プレイリスト、各ユーザーの各プレイリストの全曲をクエリするとします。ユーザーにはプレイリストとの 1 対多のリレーションがあり、プレイリストには曲との多対多のリレーションがあります。次のコード例は、これらのエンティティを表すクラスと、プレイリストと曲との多対多のリレーションを表す相互参照テーブルを示しています。
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; }
まず、通常どおり、セットの中で 2 つのテーブル間のリレーションを、データクラスと @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
リレーション クラスとの間の 1 対多のリレーションをモデル化する 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
クラスは、3 つのエンティティ クラス User
、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 内のエンティティ間のリレーションを定義する方法について詳しくは、次の参考情報をご覧ください。
サンプル
動画
- Room の新機能(Android Dev Summit '19)