オブジェクト間のリレーションを定義する

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 型のフィールドが含まれているとします。このフィールドは streetcitystatepostCode という名前のフィールドで構成されています。構成後の列をテーブル内に個別に格納するには、次のコード スニペットのように、@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 オブジェクトを表すテーブルには、idfirstNamestreetstatecitypost_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 つのエンティティ クラス UserPlaylistSong のリレーションを間接的にモデル化します。このことを図 1 に示します。

UserWithPlaylistsAndSongs は User と PlaylistWithSongs のリレーションをモデル化し、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 内のエンティティ間のリレーションを定義する方法について詳しくは、次の参考情報をご覧ください。

サンプル

動画

ブログ