Zdefiniuj relacje między obiektami

SQLite to relacyjna baza danych, więc możesz definiować relacje między encjami. Jednak większość bibliotek mapowania obiektów i relacji pozwala na odniesienie się do siebie obiektów encji, jednak funkcja Room wyraźnie zabrania tego. Aby poznać uzasadnienie techniczne tej decyzji, przeczytaj artykuł Dlaczego sala nie zezwala na odwołania do obiektów.

Dwa możliwe podejścia

W przypadku sali można na 2 sposoby definiować relacje między encjami i wysyłać dotyczące ich zapytania: przy użyciu pośredniej klasy danych z umieszczonymi obiektami lub relacyjnej metody zapytania ze zwracanym typem wielu map.

Pośrednia klasa danych

W metodzie pośredniej klasy danych definiujesz klasę danych, która modeluje relację między encjami dotyczącymi sal. Ta klasa danych łączy wystąpienia jednej encji z instancjami innej jako osadzone obiekty. Twoje metody zapytań będą mogły zwracać wystąpienia tej klasy danych do użycia w aplikacji.

Możesz np. zdefiniować klasę danych UserBook, która będzie reprezentować użytkowników biblioteki z wyrejestrowanymi książkami, oraz metodę zapytania, która pobierze listę instancji UserBook z bazy danych:

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

Zwracane typy wielu map

W metodzie zwracanego typu wielu map nie musisz określać żadnych dodatkowych klas danych. Zamiast tego definiujesz typ zwrotu multimap dla metody na podstawie żądanej struktury mapy i definiujesz relację między encjami bezpośrednio w zapytaniu SQL.

Na przykład ta metoda zapytania zwraca mapowanie wystąpień User i Book, które reprezentują użytkowników biblioteki, którzy wymeldowali się z konkretnymi książkami:

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

Wybierz metodę

Usługa Room obsługuje oba te podejścia, więc możesz wybrać tę, która sprawdzi się najlepiej w przypadku Twojej aplikacji. W tej sekcji omawiamy powody, dla których możesz używać jednego z nich.

Podejście średniowiecznej klasy danych pozwala uniknąć pisania skomplikowanych zapytań SQL, ale może też zwiększyć złożoność kodu ze względu na wymagane dodatkowe klasy danych. Krótko mówiąc, metoda zwrotu z wieloma mapami wymaga więcej zapytań SQL, a metoda pośredniej klasy danych – więcej pracy z kodem.

Jeśli nie masz konkretnego powodu, aby używać pośrednich klas danych, zalecamy użycie metody zwracania z użyciem wielu map. Więcej informacji o tym podejściu znajdziesz w artykule o zwracaniu mapy wielomapowej.

W pozostałej części tego przewodnika dowiesz się, jak definiować relacje za pomocą metody pośredniej klas danych.

Tworzenie obiektów umieszczonych

Czasami chcesz wyrazić encję lub obiekt danych jako spójną całość w ramach logiki bazy danych, nawet jeśli obiekt zawiera kilka pól. W takich sytuacjach możesz użyć adnotacji @Embedded do reprezentowania obiektu, który chcesz rozdzielić na pola podrzędne w tabeli. Możesz potem wysyłać zapytania o umieszczone pola, tak jak w przypadku innych kolumn.

Na przykład klasa User może zawierać pole typu Address, które reprezentuje kompozycję pól o nazwach street, city, state i postCode. Aby osobno przechowywać w tabeli utworzone kolumny, dodaj do klasy User pole Address z adnotacją @Embedded, jak pokazano w tym fragmencie kodu:

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

Tabela reprezentująca obiekt User zawiera kolumny o tych nazwach: id, firstName, street, state, city i post_code.

Jeśli w elemencie jest wiele umieszczonych pól tego samego typu, możesz zadbać o niepowtarzalność każdej kolumny, ustawiając właściwość prefix. Następnie dodaje podaną wartość na początku nazwy każdej kolumny w umieszczonym obiekcie.

Zdefiniuj relacje jeden do jednego

Relacja 1 do 1 między 2 elementami to relacja, w której każde wystąpienie encji nadrzędnej odpowiada dokładnie 1 wystąpieniu jednostki podrzędnej i odwrotnie.

Weźmy na przykład aplikację do strumieniowego odtwarzania muzyki, w której użytkownik ma bibliotekę swoich utworów. Każdy użytkownik ma tylko jedną bibliotekę, a każda z nich odpowiada dokładnie jednemu użytkownikowi. Dlatego między elementem User a elementem Library występuje relacja 1:1.

Aby zdefiniować relację jeden do jednego, najpierw utwórz klasę dla każdej z 2 elementów. Jeden z elementów musi zawierać zmienną, która jest odwołaniem do klucza podstawowego innego elementu.

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

Aby wysłać zapytanie o listę użytkowników i odpowiadających im bibliotek, musisz najpierw wymodelować relację jeden do jednego między 2 encjami. Aby to zrobić, utwórz nową klasę danych, w której każda instancja będzie zawierać instancję jednostki nadrzędnej i odpowiednią instancję jednostki podrzędnej. Do instancji encji podrzędnej dodaj adnotację @Relation, w której wartość parentColumn będzie odpowiadać nazwie kolumny klucza podstawowego elementu nadrzędnego, a entityColumn – nazwą kolumny jednostki podrzędnej, która odwołuje się do klucza podstawowego elementu nadrzędnego.

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

Na koniec dodaj do klasy DAO metodę, która zwraca wszystkie instancje klasy danych łączącej encję nadrzędną z podrzędną. Ta metoda wymaga wykonania 2 zapytań w ramach usługi, więc dodaj do niej adnotację @Transaction, aby cała operacja została przeprowadzona atomowo.

Kotlin

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

Java

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

Zdefiniuj relacje jeden do wielu

Relacja jeden do wielu między 2 elementami to relacja, w której każde wystąpienie encji nadrzędnej odpowiada 0 lub większej liczbie wystąpień jednostki podrzędnej, ale każde wystąpienie encji podrzędnej może odpowiadać tylko jednemu wystąpieniem elementu nadrzędnego.

Załóżmy, że w przypadku aplikacji do strumieniowego odtwarzania muzyki użytkownik może uporządkować utwory w playlisty. Każdy użytkownik może utworzyć dowolną liczbę playlist, ale każda z nich jest tworzona przez dokładnie jednego użytkownika. Dlatego między elementem User a elementem Playlist występuje relacja jeden do wielu.

Aby zdefiniować relację jeden do wielu, najpierw utwórz klasę dla tych dwóch encji. Tak jak w relacji jeden do jednego, encja podrzędna musi zawierać zmienną będącą odwołaniem do klucza podstawowego jednostki nadrzędnej.

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

Aby przesłać zapytanie dotyczące listy użytkowników i odpowiadających im playlist, musisz najpierw wymodelować relację jeden do wielu między 2 elementami. W tym celu utwórz nową klasę danych, w której każda instancja będzie zawierać instancję encji nadrzędnej oraz listę wszystkich odpowiadających jej instancji podrzędnych. Do instancji encji podrzędnej dodaj adnotację @Relation, w której wartość parentColumn będzie odpowiadać nazwie kolumny klucza podstawowego elementu nadrzędnego, a entityColumn – nazwą kolumny jednostki podrzędnej, która odwołuje się do klucza podstawowego elementu nadrzędnego.

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

Na koniec dodaj do klasy DAO metodę, która zwraca wszystkie instancje klasy danych łączącej encję nadrzędną z podrzędną. Ta metoda wymaga wykonania 2 zapytań w ramach usługi, więc dodaj do niej adnotację @Transaction, aby cała operacja została przeprowadzona atomowo.

Kotlin

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

Java

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

Zdefiniuj relacje wiele do wielu

Relacja wiele do wielu między 2 elementami to relacja, w której każde wystąpienie encji nadrzędnej odpowiada 0 lub większej liczbie wystąpień jednostki podrzędnej, a odwrotność jest również rzeczywista.

W przykładzie aplikacji do strumieniowego odtwarzania muzyki przyjrzyjmy się utworom z playlist zdefiniowanych przez użytkownika. Każda playlista może zawierać wiele utworów, a każdy z nich może być częścią wielu różnych playlist. Dlatego między elementem Playlist a elementem Song istnieje relacja wiele do wielu.

Aby zdefiniować relację wiele do wielu, najpierw utwórz klasę dla każdej z 2 elementów. Relacje wiele do wielu różnią się od innych typów relacji, ponieważ zasadniczo nie zawierają odniesienia do encji nadrzędnej. Zamiast tego utwórz trzecią klasę, która będzie reprezentować jednostkę powiązaną lub tabelę porównawczą między tymi 2 elementami. Tabela odniesień musi zawierać kolumny klucza podstawowego z każdego elementu w relacji wiele do wielu reprezentowanych w tabeli. W tym przykładzie każdy wiersz w tabeli referencyjnej odpowiada parze wystąpienia Playlist i instancji Song, gdzie wskazany utwór znajduje się na wymienionej playliście.

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

Następny krok zależy od tego, jak chcesz wysyłać zapytania dotyczące tych powiązanych elementów.

  • Jeśli chcesz utworzyć zapytanie o playlisty i listę odpowiednich utworów na potrzeby każdej playlisty, utwórz nową klasę danych zawierającą pojedynczy obiekt Playlist i listę wszystkich obiektów Song znajdujących się na playliście.
  • Jeśli chcesz wysłać zapytania dotyczące utworów i listy odpowiadających im playlist, utwórz nową klasę danych zawierającą pojedynczy obiekt Song i listę wszystkich obiektów Playlist, w których znajduje się dany utwór.

W obu przypadkach modeluj relację między encjami, korzystając z właściwości associateBy w adnotacji @Relation w każdej z tych klas, aby zidentyfikować encję referencyjną będącą źródłem relacji między elementem Playlist a encją 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;
}

Na koniec dodaj metodę do klasy DAO, aby udostępnić funkcje zapytań, których potrzebuje Twoja aplikacja.

  • getPlaylistsWithSongs: ta metoda wysyła zapytanie do bazy danych i zwraca wszystkie powstałe w ten sposób obiekty PlaylistWithSongs.
  • getSongsWithPlaylists: ta metoda wysyła zapytanie do bazy danych i zwraca wszystkie powstałe w ten sposób obiekty SongWithPlaylists.

Każda z tych metod wymaga wykonania 2 zapytań w pomieszczeniu, więc dodaj do obu metod adnotację @Transaction, aby cała operacja była wykonywana atomowo.

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

Zdefiniuj relacje zagnieżdżone

Czasem możesz chcieć wysłać zapytanie do zestawu 3 lub większej liczby tabel, które są ze sobą powiązane. W takim przypadku definiujesz relacje zagnieżdżone między tabelami.

Załóżmy, że w przypadku aplikacji do strumieniowego odtwarzania muzyki chcesz wysłać zapytanie do wszystkich użytkowników, wszystkich playlist każdego użytkownika oraz wszystkich utworów na każdej playliście w przypadku każdego użytkownika. Użytkownicy korzystają z playlist w relacji jeden do wielu, a playlisty – wiele do wielu z utworami. Poniższy przykładowy kod pokazuje klasy reprezentujące te elementy, a także tabelę odsyłającą dla relacji wiele do wielu między playlistami a utworami:

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

Najpierw w zwykły sposób wymodeluj relację między 2 tabelami w zbiorze, korzystając z klasy danych i adnotacji @Relation. Poniższy przykład pokazuje klasę PlaylistWithSongs, która modeluje relację wiele do wielu między klasą encji Playlist a klasą encji Song:

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

Po zdefiniowaniu klasy danych, która reprezentuje tę relację, utwórz kolejną klasę danych, która modeluje relację między inną tabelą z Twojego zbioru a pierwszą klasą relacji, „zagnieżdżając” istniejącą relację w nowej. Poniższy przykład pokazuje klasę UserWithPlaylistsAndSongs, która modeluje relację jeden do wielu między klasą encji User i klasą relacji PlaylistWithSongs:

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

Klasa UserWithPlaylistsAndSongs pośrednio modeluje relacje między wszystkimi 3 klasami encji: User, Playlist i Song. Zostało to pokazane na ilustracji 1.

Element UserWithPlayAndSongs modeluje relację między użytkownikiem a playlistą PlaylistWithSongs, która z kolei modeluje relację między playlistą a utworem.

Rysunek 1. Schemat klas relacji w przykładowej aplikacji do strumieniowego odtwarzania muzyki.

Jeśli w zestawie jest więcej tabel, utwórz klasę do modelowania zależności między wszystkimi pozostałymi tabelami a klasą relacji, która modeluje relacje między wszystkimi poprzednimi tabelami. Spowoduje to utworzenie łańcucha zagnieżdżonych relacji między wszystkimi tabelami, których ma dotyczyć zapytanie.

Na koniec dodaj metodę do klasy DAO, aby udostępnić funkcję zapytań, której potrzebuje Twoja aplikacja. Ta metoda wymaga wykonania wielu zapytań w komponencie, więc dodaj adnotację @Transaction, aby cała operacja została przeprowadzona atomowo:

Kotlin

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

Java

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

Dodatkowe materiały

Aby dowiedzieć się więcej o definiowaniu relacji między elementami w salach, zapoznaj się z poniższymi dodatkowymi zasobami.

Próbki

Filmy

Blogi