Definisci le relazioni tra gli oggetti

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 oggetti Song 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 oggetti Playlist 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 oggetti PlaylistWithSongs risultanti.
  • getSongsWithPlaylists: questo metodo esegue una query sul database e restituisce tutti gli oggetti SongWithPlaylists 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.

UserWithPlaylistAndSongs modella la relazione tra User e PlaylistWithSongs, che a sua volta modella la relazione tra Playlist e Song.

Figura 1. Diagramma delle classi di relazione nell'esempio dell'app di streaming musicale.

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

Blog