Beziehungen zwischen Objekten definieren

Da SQLite eine relationale Datenbank ist, können Sie Beziehungen zwischen Entitäten definieren. Obwohl die meisten objektrelationalen Zuordnungsbibliotheken Entitätsobjekte aufeinander verweisen lassen, untersagt Room dies explizit. Weitere Informationen zu den technischen Gründen für diese Entscheidung finden Sie unter Warum ist Raum für keine Objektverweise zulässig?.

Zwei mögliche Ansätze

In Room gibt es zwei Möglichkeiten, eine Beziehung zwischen Entitäten zu definieren und abzufragen: Sie können entweder eine Zwischendatenklasse mit eingebetteten Objekten oder eine relationale Abfragemethode mit einem Multimap-Rückgabetyp verwenden.

Zwischendatenklasse

Beim Datenklassen-Zwischenansatz definieren Sie eine Datenklasse, die die Beziehung zwischen Ihren Zimmerentitäten modelliert. Diese Datenklasse enthält die Kopplungen zwischen Instanzen einer Entität und Instanzen einer anderen Entität als eingebettete Objekte. Ihre Abfragemethoden können dann Instanzen dieser Datenklasse zur Verwendung in Ihrer Anwendung zurückgeben.

Beispielsweise kannst du eine UserBook-Datenklasse definieren, die Bibliotheksnutzer mit bestimmten ausgecheckten Büchern darstellt, und eine Abfragemethode definieren, um eine Liste von UserBook-Instanzen aus der Datenbank abzurufen:

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;
}

Multimap-Rückgabetypen

Beim Ansatz des Multimap-Rückgabetyps müssen Sie keine zusätzlichen Datenklassen definieren. Stattdessen definieren Sie einen multimap-Rückgabetyp für Ihre Methode auf Grundlage der gewünschten Kartenstruktur und definieren die Beziehung zwischen den Entitäten direkt in Ihrer SQL-Abfrage.

Die folgende Abfragemethode gibt beispielsweise eine Zuordnung von User- und Book-Instanzen zurück, um Bibliotheksnutzer mit bestimmten ausgecheckten Büchern darzustellen:

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

Ansatz auswählen

Room unterstützt beide Ansätze. Sie können also denjenigen verwenden, der für Ihre Anwendung am besten geeignet ist. In diesem Abschnitt werden einige Gründe erläutert, warum Sie den einen oder den anderen bevorzugen.

Mit dem Ansatz mit zwischengeschalteten Datenklassen können Sie das Schreiben komplexer SQL-Abfragen vermeiden. Aufgrund der zusätzlichen erforderlichen Datenklassen kann dies aber auch zu einer höheren Codekomplexität führen. Kurz gesagt erfordert der Multimap-Rückgabeansatz, dass Ihre SQL-Abfragen mehr Arbeit machen, und der Ansatz mit der Datenklasse erfordert, dass Ihr Code mehr Arbeit erledigt.

Wenn Sie keinen bestimmten Grund für die Verwendung von Zwischendatenklassen haben, empfehlen wir den Multimap-Rückgabetyp. Weitere Informationen zu diesem Ansatz finden Sie unter Multimap zurückgeben.

Im weiteren Verlauf dieser Anleitung wird gezeigt, wie Beziehungen mithilfe des Ansatzes mit Zwischendatenklassen definiert werden.

Eingebettete Objekte erstellen

Manchmal möchten Sie eine Entität oder ein Datenobjekt als zusammenhängendes Ganzes in Ihrer Datenbanklogik ausdrücken, auch wenn das Objekt mehrere Felder enthält. In diesen Fällen können Sie mit der Annotation @Embedded ein Objekt darstellen, das Sie innerhalb einer Tabelle in seine untergeordneten Felder zerlegen möchten. Anschließend können Sie die eingebetteten Felder wie andere einzelne Spalten abfragen.

Die Klasse User kann beispielsweise ein Feld vom Typ Address enthalten, das eine Zusammensetzung von Feldern namens street, city, state und postCode darstellt. Wenn Sie die zusammengesetzten Spalten separat in der Tabelle speichern möchten, fügen Sie in die Klasse User das Feld Address ein, das mit @Embedded annotiert ist, wie im folgenden Code-Snippet gezeigt:

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;
}

Die Tabelle, die ein User-Objekt darstellt, enthält dann Spalten mit den folgenden Namen: id, firstName, street, state, city und post_code.

Wenn eine Entität mehrere eingebettete Felder desselben Typs hat, können Sie dafür sorgen, dass jede Spalte eindeutig bleibt. Legen Sie dazu das Attribut prefix fest. Room fügt dann den angegebenen Wert am Anfang jedes Spaltennamens im eingebetteten Objekt hinzu.

1:1-Beziehungen definieren

Bei einer 1:1-Beziehung zwischen zwei Entitäten handelt es sich um eine Beziehung, bei der jede Instanz der übergeordneten Entität genau einer Instanz der untergeordneten Entität entspricht und umgekehrt.

Nehmen wir als Beispiel eine Musikstreaming-App, in der der Nutzer eine Mediathek mit eigenen Titeln hat. Jeder Nutzer hat nur eine Bibliothek und jede Bibliothek entspricht genau einem Nutzer. Daher besteht eine 1:1-Beziehung zwischen der Entität User und der Entität Library.

Zum Definieren einer 1:1-Beziehung erstellen Sie zuerst eine Klasse für jede der beiden Entitäten. Eine der Entitäten muss eine Variable enthalten, die ein Verweis auf den Primärschlüssel der anderen Entität ist.

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;
}

Um die Liste der Nutzer und der entsprechenden Bibliotheken abzufragen, müssen Sie zuerst die 1:1-Beziehung zwischen den beiden Entitäten modellieren. Erstellen Sie dazu eine neue Datenklasse, in der jede Instanz eine Instanz der übergeordneten und die entsprechende Instanz der untergeordneten Entität enthält. Fügen Sie der Instanz der untergeordneten Entität die Annotation @Relation hinzu, wobei parentColumn auf den Namen der Primärschlüsselspalte der übergeordneten Entität und entityColumn auf den Namen der Spalte der untergeordneten Entität festgelegt ist, die auf den Primärschlüssel der übergeordneten Entität verweist.

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;
}

Fügen Sie schließlich der DAO-Klasse eine Methode hinzu, die alle Instanzen der Datenklasse zurückgibt, die die übergeordnete und die untergeordnete Entität paart. Bei dieser Methode muss Room zwei Abfragen ausführen. Daher fügen Sie dieser Methode die Annotation @Transaction hinzu, damit der gesamte Vorgang atomar ausgeführt wird.

Kotlin

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

Java

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

1:n-Beziehungen definieren

Bei einer 1:n-Beziehung zwischen zwei Entitäten handelt es sich um eine Beziehung, bei der jede Instanz der übergeordneten Entität null oder mehr Instanzen der untergeordneten Entität entspricht, aber jede Instanz der untergeordneten Entität nur genau einer Instanz der übergeordneten Entität entsprechen kann.

Angenommen, der Nutzer hat in der Musikstreaming-App die Möglichkeit, seine Songs in Playlists zu organisieren. Jeder Nutzer kann beliebig viele Playlists erstellen, jedoch wird jede Playlist von genau einem Nutzer erstellt. Daher besteht eine 1:n-Beziehung zwischen der Entität User und der Entität Playlist.

Um eine 1:n-Beziehung zu definieren, erstellen Sie zunächst eine Klasse für die beiden Entitäten. Wie bei einer 1:1-Beziehung muss die untergeordnete Entität eine Variable enthalten, die ein Verweis auf den Primärschlüssel der übergeordneten Entität ist.

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;
}

Wenn Sie die Liste der Nutzer und der entsprechenden Playlists abfragen möchten, müssen Sie zuerst die 1:n-Beziehung zwischen den beiden Entitäten modellieren. Dazu erstellen Sie eine neue Datenklasse, in der jede Instanz eine Instanz der übergeordneten Entität und eine Liste aller zugehörigen Instanzen der untergeordneten Entität enthält. Fügen Sie der Instanz der untergeordneten Entität die Annotation @Relation hinzu, wobei parentColumn auf den Namen der Primärschlüsselspalte der übergeordneten Entität und entityColumn auf den Namen der Spalte der untergeordneten Entität festgelegt ist, die auf den Primärschlüssel der übergeordneten Entität verweist.

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;
}

Fügen Sie schließlich der DAO-Klasse eine Methode hinzu, die alle Instanzen der Datenklasse zurückgibt, die die übergeordnete und die untergeordnete Entität paart. Bei dieser Methode muss Room zwei Abfragen ausführen. Daher fügen Sie dieser Methode die Annotation @Transaction hinzu, damit der gesamte Vorgang atomar ausgeführt wird.

Kotlin

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

Java

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

m:n-Beziehungen definieren

Bei einer m:n-Beziehung zwischen zwei Entitäten handelt es sich um eine Beziehung, bei der jede Instanz der übergeordneten Entität null oder mehr Instanzen der untergeordneten Entität entspricht und umgekehrt.

Betrachten Sie im Beispiel der Musikstreaming-App die Titel in den benutzerdefinierten Playlists. Jede Playlist kann viele Titel enthalten und jeder Titel kann Teil vieler verschiedener Playlists sein. Daher besteht eine n:n-Beziehung zwischen der Entität Playlist und der Entität Song.

Um eine n:n-Beziehung zu definieren, müssen Sie zuerst für jede der beiden Entitäten eine Klasse erstellen. m:n-Beziehungen unterscheiden sich von anderen Beziehungstypen, da im Allgemeinen kein Verweis auf die übergeordnete Entität in der untergeordneten Entität vorhanden ist. Erstellen Sie stattdessen eine dritte Klasse, um eine assoziative Entität oder Querverweistabelle zwischen den beiden Entitäten darzustellen. Die Querverweistabelle muss Spalten für den Primärschlüssel aus jeder Entität in der m:n-Beziehung haben, die in der Tabelle dargestellt wird. In diesem Beispiel entspricht jede Zeile in der Querverweistabelle einem Paar aus einer Playlist- und einer Song-Instanz, bei der der referenzierte Titel in der referenzierten Playlist enthalten ist.

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;
}

Der nächste Schritt hängt davon ab, wie Sie diese verwandten Entitäten abfragen möchten.

  • Wenn Sie für jede Playlist playlists und eine Liste der entsprechenden Songs abfragen möchten, erstellen Sie eine neue Datenklasse, die ein einzelnes Playlist-Objekt und eine Liste aller Song-Objekte in der Playlist enthält.
  • Wenn Sie Songs und eine Liste der entsprechenden Playlists abfragen möchten, erstellen Sie eine neue Datenklasse, die ein einzelnes Song-Objekt und eine Liste aller Playlist-Objekte enthält, in denen der Song enthalten ist.

In beiden Fällen wird die Beziehung zwischen den Entitäten modelliert. Verwenden Sie dazu das Attribut associateBy in der Annotation @Relation in jeder dieser Klassen, um die Querverweisentität zu identifizieren, die die Beziehung zwischen der Entität Playlist und der Entität Song bereitstellt.

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;
}

Fügen Sie schließlich der DAO-Klasse eine Methode hinzu, um die Abfragefunktionalität verfügbar zu machen, die die Anwendung benötigt.

  • getPlaylistsWithSongs: Diese Methode fragt die Datenbank ab und gibt alle resultierenden PlaylistWithSongs-Objekte zurück.
  • getSongsWithPlaylists: Diese Methode fragt die Datenbank ab und gibt alle resultierenden SongWithPlaylists-Objekte zurück.

Bei diesen Methoden muss Room jeweils zwei Abfragen ausführen. Fügen Sie daher beiden Methoden die Annotation @Transaction hinzu, damit der gesamte Vorgang atomar ausgeführt wird.

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

Verschachtelte Beziehungen definieren

Manchmal müssen Sie möglicherweise eine Gruppe von drei oder mehr Tabellen abfragen, die alle miteinander verknüpft sind. In diesem Fall definieren Sie verschachtelte Beziehungen zwischen den Tabellen.

Angenommen, Sie möchten in der Musikstreaminganwendung alle Nutzer, alle Playlists für jeden Nutzer und alle Titel in jeder Playlist für jeden Nutzer abfragen. Nutzer haben eine 1:n-Beziehung zu Playlists und Playlists eine m:n-Beziehung zu Songs. Das folgende Codebeispiel zeigt die Klassen, die diese Entitäten darstellen, sowie die Querverweistabelle für die m:n-Beziehung zwischen Playlists und Songs:

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;
}

Modellieren Sie zuerst die Beziehung zwischen zwei der Tabellen in Ihrem Dataset wie gewohnt. Verwenden Sie dazu eine Datenklasse und die Annotation @Relation. Das folgende Beispiel zeigt eine PlaylistWithSongs-Klasse, die eine m:n-Beziehung zwischen der Entitätsklasse Playlist und der Entitätsklasse Song modelliert:

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;
}

Nachdem Sie eine Datenklasse definiert haben, die diese Beziehung darstellt, erstellen Sie eine weitere Datenklasse, die die Beziehung zwischen einer anderen Tabelle aus Ihrem Satz und der ersten Beziehungsklasse modelliert. Verschachteln Sie dann die vorhandene Beziehung in der neuen. Das folgende Beispiel zeigt eine UserWithPlaylistsAndSongs-Klasse, die eine 1:n-Beziehung zwischen der Entitätsklasse User und der Beziehungsklasse PlaylistWithSongs modelliert:

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;
}

Die UserWithPlaylistsAndSongs-Klasse modelliert indirekt die Beziehungen zwischen allen drei Entitätsklassen: User, Playlist und Song. Das ist in Abbildung 1 dargestellt.

UserWithPlaylistsAndSongs modelliert die Beziehung zwischen „User“ und „PlaylistWithSongs“, die wiederum die Beziehung zwischen „Playlist“ und „Titel“ modelliert.

Abbildung 1: Diagramm der Beziehungsklassen im Beispiel der Musikstreaming-App

Wenn Ihr Dataset weitere Tabellen enthält, erstellen Sie eine Klasse, um die Beziehung zwischen den verbleibenden Tabellen und der Beziehungsklasse zu modellieren, die die Beziehungen zwischen allen vorherigen Tabellen modelliert. Dadurch wird eine Kette verschachtelter Beziehungen zwischen allen Tabellen erstellt, die Sie abfragen möchten.

Fügen Sie schließlich der DAO-Klasse eine Methode hinzu, um die von Ihrer Anwendung benötigte Abfragefunktion bereitzustellen. Bei dieser Methode muss Room mehrere Abfragen ausführen. Fügen Sie daher die Annotation @Transaction hinzu, damit der gesamte Vorgang atomar ausgeführt wird:

Kotlin

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

Java

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

Weitere Informationen

Weitere Informationen zum Definieren von Beziehungen zwischen Entitäten in Room finden Sie in den folgenden zusätzlichen Ressourcen.

Produktproben

Videos

Blogs