Poiché SQLite è un database relazionale, puoi definire le relazioni tra le entità. Tuttavia, sebbene la maggior parte delle librerie di mapping relazionale degli oggetti consenta agli oggetti entità di fare riferimento l'uno all'altro, Room vieta esplicitamente questo. Per conoscere il ragionamento tecnico alla base di questa decisione, vedi Comprendere perché la stanza virtuale non consente i riferimenti agli oggetti.
Due possibili approcci
In Room, esistono due modi per definire ed eseguire query su una relazione tra entità: utilizzando una classe dati intermedia con oggetti incorporati o un metodo di query relazionale con un tipo restituito multimappa.
Classe dati intermedia
Nell'approccio delle classi di dati intermedi, definisci una classe di dati che modella la relazione tra le entità Room. Questa classe di dati contiene gli abbinamenti tra le istanze di un'entità e le istanze di un'altra entità come oggetti incorporati. I metodi di query possono quindi restituire istanze di questa classe di dati per utilizzarle nella tua app.
Ad esempio, puoi definire una classe di dati UserBook
per rappresentare gli utenti della biblioteca con libri specifici pagati e definire un metodo di query per recuperare un elenco di istanze UserBook
dal database:
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; }
Tipi restituiti multimappa
Con l'approccio del tipo restituito multimappa, non è necessario definire classi di dati aggiuntive. Puoi invece definire un tipo di restituzione multimap per il metodo in base alla struttura della mappa desiderata e definire la relazione tra le entità direttamente nella query SQL.
Ad esempio, il seguente metodo di query restituisce una mappatura delle istanze User
e Book
per rappresentare gli utenti della biblioteca con libri specifici pagati:
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();
Scegli un approccio
Room supporta entrambi gli approcci, quindi puoi scegliere quello più adatto alla tua app. Questa sezione illustra alcuni dei motivi per cui potresti preferire l'uno o l'altro.
L'approccio delle classi di dati intermedie ti consente di evitare di scrivere query SQL complesse, ma può anche comportare una maggiore complessità del codice dovuta alle classi di dati aggiuntive necessarie. In breve, l'approccio del tipo di ritorno multimappa richiede che le query SQL svolgano più lavoro, mentre l'approccio della classe di dati intermedi richiede il tuo codice per aumentare l'efficienza.
Se non hai un motivo specifico per utilizzare classi di dati intermedie, ti consigliamo di utilizzare l'approccio del tipo restituito multimappa. Per scoprire di più su questo approccio, consulta Restituire una mappa multipla.
Il resto di questa guida mostra come definire le relazioni utilizzando l'approccio alla classe di dati intermedi.
Crea oggetti incorporati
A volte potresti voler esprimere un'entità o un oggetto dati come un insieme coeso nella logica del database, anche se l'oggetto contiene più campi. In questi casi, puoi utilizzare l'annotazione
@Embedded
per rappresentare un oggetto che vuoi scomporre nei suoi
sottocampi all'interno di una tabella. Puoi quindi eseguire query nei campi incorporati come faresti per le altre singole colonne.
Ad esempio, la classe User
può includere un campo di tipo Address
che
rappresenta una composizione di campi denominati street
, city
, state
e
postCode
. Per archiviare separatamente le colonne composte nella tabella, includi un campo Address
nella classe User
annotata con @Embedded
, come mostrato nel seguente snippet di codice:
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; }
La tabella che rappresenta un oggetto User
contiene quindi colonne con i seguenti nomi: id
, firstName
, street
, state
, city
e post_code
.
Se un'entità ha più campi incorporati dello stesso tipo, puoi mantenere univoca ogni colonna impostando la proprietà prefix
. La stanza virtuale aggiunge quindi il valore fornito all'inizio del nome di ogni colonna nell'oggetto incorporato.
Definire le relazioni uno a uno
Una relazione uno a uno tra due entità è una relazione in cui ogni istanza dell'entità padre corrisponde esattamente a un'istanza dell'entità figlio e si verifica anche il contrario.
Prendiamo come esempio un'app di streaming musicale in cui l'utente ha una raccolta di brani di sua proprietà. Ogni utente ha una sola libreria e ogni libreria
corrisponde esattamente a un utente. Pertanto, esiste una relazione one-to-one tra l'entità User
e l'entità Library
.
Per definire una relazione uno a uno, devi prima creare una classe per ciascuna delle due entità. Una delle entità deve includere una variabile che faccia riferimento alla chiave primaria dell'altra entità.
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; }
Per eseguire query sull'elenco degli utenti e sulle librerie corrispondenti, devi prima modellare la relazione one-to-one tra le due entità. A questo scopo, crea una nuova classe di dati in cui ogni istanza contiene un'istanza dell'entità padre e l'istanza corrispondente dell'entità figlio. Aggiungi l'annotazione @Relation
all'istanza dell'entità figlio, con parentColumn
impostato sul nome della colonna di chiave primaria dell'entità padre e entityColumn
impostato sul nome della colonna dell'entità figlio che fa riferimento alla chiave primaria dell'entità padre.
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; }
Infine, aggiungi un metodo alla classe DAO che restituisca tutte le istanze della classe dati che accoppiano l'entità padre e l'entità figlio. Questo metodo richiede uno spazio per eseguire due query, quindi aggiungi l'annotazione @Transaction
a questo metodo in modo che l'intera operazione venga eseguita a livello atomico.
Kotlin
@Transaction @Query("SELECT * FROM User") fun getUsersAndLibraries(): List<UserAndLibrary>
Java
@Transaction @Query("SELECT * FROM User") public List<UserAndLibrary> getUsersAndLibraries();
Definire le relazioni one-to-many
Una relazione one-to-many tra due entità è una relazione in cui ogni istanza dell'entità padre corrisponde a zero o a più istanze dell'entità figlio, ma ogni istanza dell'entità figlio può corrispondere esattamente a una sola istanza dell'entità padre.
Nell'esempio dell'app di streaming musicale, supponiamo che l'utente abbia la possibilità di organizzare i brani in playlist. Ogni utente può creare tutte le playlist che vuole,
ma ogni playlist è creata da un solo utente. Pertanto, esiste una relazione one-to-many tra l'entità User
e l'entità Playlist
.
Per definire una relazione one-to-many, devi prima creare una classe per le due entità. Come in una relazione one-to-one, l'entità figlio deve includere una variabile che fa riferimento alla chiave primaria dell'entità padre.
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; }
Per eseguire query sull'elenco di utenti e sulle playlist corrispondenti, devi prima modellare la relazione one-to-many tra le due entità. A questo scopo, crea una nuova classe di dati in cui ogni istanza contiene un'istanza dell'entità padre e un elenco di tutte le istanze dell'entità figlio corrispondenti. Aggiungi l'annotazione @Relation
all'istanza dell'entità figlio, con parentColumn
impostato sul nome della colonna di chiave primaria dell'entità padre e entityColumn
impostato sul nome della colonna dell'entità figlio che fa riferimento alla chiave primaria dell'entità padre.
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; }
Infine, aggiungi un metodo alla classe DAO che restituisca tutte le istanze della classe dati che accoppiano l'entità padre e l'entità figlio. Questo metodo richiede uno spazio per eseguire due query, quindi aggiungi l'annotazione @Transaction
a questo metodo in modo che l'intera operazione venga eseguita a livello atomico.
Kotlin
@Transaction @Query("SELECT * FROM User") fun getUsersWithPlaylists(): List<UserWithPlaylists>
Java
@Transaction @Query("SELECT * FROM User") public List<UserWithPlaylists> getUsersWithPlaylists();
Definire le relazioni many-to-many
Una relazione many-to-many tra due entità è una relazione in cui ogni istanza dell'entità padre corrisponde a zero o più istanze dell'entità figlio e anche il contrario è vero.
Nell'esempio dell'app di streaming musicale, considera i brani nelle playlist definite dall'utente.
Ogni playlist può contenere molti brani e ogni brano può far parte di tante playlist diverse. Pertanto, esiste una relazione many-to-many
tra l'entità Playlist
e l'entità Song
.
Per definire una relazione many-to-many, devi prima creare una classe per ciascuna delle due entità. Le relazioni many-to-many
sono distinte dagli altri tipi di relazioni perché in genere non c'è alcun riferimento
all'entità padre nell'entità figlio. Crea invece una terza classe per rappresentare un'entità associata, o tabella di riferimento incrociato, tra le due entità. La tabella di riferimento incrociato deve contenere colonne per la chiave primaria di ciascuna entità nella relazione many-to-many rappresentata nella tabella. In questo esempio, ogni riga nella tabella di riferimenti incrociati corrisponde a un accoppiamento tra un'istanza Playlist
e un'istanza Song
in cui il brano a cui viene fatto riferimento è incluso nella playlist di riferimento.
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; }
Il passaggio successivo dipende dal modo in cui vuoi eseguire query su queste entità correlate.
- Se vuoi eseguire query sulle playlist e su un elenco dei brani corrispondenti per
ogni playlist, crea una nuova classe di dati che contenga un singolo oggetto
Playlist
e un elenco di tutti gli oggettiSong
inclusi nella playlist. - Se vuoi eseguire query su brani e un elenco delle playlist corrispondenti per ciascuna, crea una nuova classe di dati contenente un singolo oggetto
Song
e un elenco di tutti gli oggettiPlaylist
in cui è incluso il brano.
In entrambi i casi, modella la relazione tra le entità utilizzando la proprietà associateBy
nell'annotazione @Relation
in ciascuna di queste classi per identificare l'entità di riferimento incrociato che fornisce la relazione tra l'entità Playlist
e l'entità 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; }
Infine, aggiungi un metodo alla classe DAO per esporre la funzionalità di query necessaria all'app.
getPlaylistsWithSongs
: questo metodo esegue una query sul database e restituisce tutti gli oggettiPlaylistWithSongs
risultanti.getSongsWithPlaylists
: questo metodo esegue una query sul database e restituisce tutti gli oggettiSongWithPlaylists
risultanti.
Questi metodi richiedono ognuno la stanza virtuale per eseguire due query, quindi aggiungi l'annotazione @Transaction
a entrambi i metodi in modo che l'intera operazione venga eseguita a livello atomico.
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();
Definisci relazioni nidificate
A volte, potrebbe essere necessario eseguire una query su un insieme di tre o più tabelle tutte correlate tra loro. In tal caso, devi definire le relazioni nidificate tra le tabelle.
Supponiamo che, nell'esempio dell'app di streaming musicale, vuoi eseguire una query a tutti gli utenti, a tutte le playlist di ogni utente e a tutti i brani in ogni playlist per ogni utente. Gli utenti hanno un rapporto one-to-many con le playlist, mentre le playlist hanno un rapporto many-to-many con i brani. Il seguente esempio di codice mostra le classi che rappresentano queste entità e la tabella di riferimento incrociato per la relazione many-to-many tra playlist e brani:
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; }
Innanzitutto, modella la relazione tra due delle tabelle nel set come fai normalmente, utilizzando una classe di dati e l'annotazione @Relation
. L'esempio seguente mostra una classe PlaylistWithSongs
che modella una relazione many-to-many tra la classe di entità Playlist
e la classe dell'entità 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; }
Dopo aver definito una classe di dati che rappresenta questa relazione, crea un'altra classe di dati che modelli la relazione tra un'altra tabella del tuo set e la prima classe di relazione, "nidificando" la relazione esistente all'interno di quella nuova. L'esempio seguente mostra una classe UserWithPlaylistsAndSongs
che modella una relazione one-to-many tra la classe dell'entità User
e la classe della relazione 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; }
La classe UserWithPlaylistsAndSongs
modella indirettamente le relazioni
tra tutte e tre le classi di entità: User
, Playlist
e Song
. come illustrato nella Figura 1.
Se ci sono altre tabelle nel set, crea una classe per modellare la relazione tra ogni tabella rimanente e la classe di relazione che modella le relazioni tra tutte le tabelle precedenti. Questo crea una catena di relazioni nidificate tra tutte le tabelle su cui vuoi eseguire la query.
Infine, aggiungi un metodo alla classe DAO per esporre la funzionalità di query necessaria all'app. Questo metodo richiede la stanza virtuale per eseguire più query, quindi aggiungi l'annotazione @Transaction
in modo che l'intera operazione venga eseguita in modo atomico:
Kotlin
@Transaction @Query("SELECT * FROM User") fun getUsersWithPlaylistsAndSongs(): List<UserWithPlaylistsAndSongs>
Java
@Transaction @Query("SELECT * FROM User") public List<UserWithPlaylistsAndSongs> getUsersWithPlaylistsAndSongs();
Risorse aggiuntive
Per scoprire di più sulla definizione delle relazioni tra entità in una stanza virtuale, consulta le seguenti risorse aggiuntive.
Samples
Video
- Novità di Room (Android Dev Summit '19)