Fare riferimento a dati complessi utilizzando Room

La camera offre funzionalità per la conversione tra tipi primitivi e box, ma non consente riferimenti a oggetti tra entità. Questo documento spiega come utilizzare i convertitori dei tipi e perché Room non supporta i riferimenti agli oggetti.

Utilizzare convertitori dei tipi

A volte è necessario che l'app memorizzi un tipo di dati personalizzato in una singola colonna del database. Puoi supportare i tipi personalizzati fornendo convertitori dei tipi, ovvero metodi che indicano a Room come convertire tipi personalizzati da e verso tipi noti che la stanza può essere permanente. Puoi identificare gli utenti che hanno completato una conversione utilizzando l'annotazione @TypeConverter.

Supponi di aver bisogno di salvare le istanze di Date nel database della stanza virtuale. La stanza virtuale non sa come rendere persistenti Date oggetti, quindi è necessario definire i convertitori dei tipi:

Kotlin

class Converters {
  @TypeConverter
  fun fromTimestamp(value: Long?): Date? {
    return value?.let { Date(it) }
  }

  @TypeConverter
  fun dateToTimestamp(date: Date?): Long? {
    return date?.time?.toLong()
  }
}

Java

public class Converters {
  @TypeConverter
  public static Date fromTimestamp(Long value) {
    return value == null ? null : new Date(value);
  }

  @TypeConverter
  public static Long dateToTimestamp(Date date) {
    return date == null ? null : date.getTime();
  }
}

Questo esempio definisce due metodi di conversione dei tipi: uno che converte un oggetto Date in un oggetto Long e uno che esegue la conversione inversa da Long a Date. Poiché Room sa come rendere persistenti Long oggetti, può utilizzare questi convertitori per rendere persistenti Date oggetti.

Successivamente, aggiungi l'annotazione @TypeConverters alla classe AppDatabase in modo che Room possa conoscere la classe di convertitori che hai definito:

Kotlin

@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
  abstract fun userDao(): UserDao
}

Java

@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
  public abstract UserDao userDao();
}

Con questi convertitori dei tipi definiti, puoi utilizzare il tipo personalizzato nelle entità e nei DAO come faresti con i tipi primitivi:

Kotlin

@Entity
data class User(private val birthday: Date?)

@Dao
interface UserDao {
  @Query("SELECT * FROM user WHERE birthday = :targetDate")
  fun findUsersBornOnDate(targetDate: Date): List<User>
}

Java

@Entity
public class User {
  private Date birthday;
}

@Dao
public interface UserDao {
  @Query("SELECT * FROM user WHERE birthday = :targetDate")
  List<User> findUsersBornOnDate(Date targetDate);
}

In questo esempio, la camera può utilizzare ovunque il convertitore di tipi definiti perché hai annotato AppDatabase con @TypeConverters. Tuttavia, puoi anche limitare l'ambito dei convertitori di tipi a entità o DAO specifiche annotando le classi @Entity o @Dao con @TypeConverters.

Inizializzazione del convertitore del tipo di controllo

In genere, Room gestisce la creazione di istanze dei convertitori dei tipi per te. Tuttavia, a volte potresti dover trasferire dipendenze aggiuntive alle classi di conversione dei tipi, il che significa che la tua app deve controllare direttamente l'inizializzazione dei convertitori dei tipi. In questo caso, annota la classe dell'autore della conversione con @ProvidedTypeConverter:

Kotlin

@ProvidedTypeConverter
class ExampleConverter {
  @TypeConverter
  fun StringToExample(string: String?): ExampleType? {
    ...
  }

  @TypeConverter
  fun ExampleToString(example: ExampleType?): String? {
    ...
  }
}

Java

@ProvidedTypeConverter
public class ExampleConverter {
  @TypeConverter
  public Example StringToExample(String string) {
    ...
  }

  @TypeConverter
  public String ExampleToString(Example example) {
    ...
  }
}

Quindi, oltre a dichiarare la classe del convertitore in @TypeConverters, utilizza il metodo RoomDatabase.Builder.addTypeConverter() per passare un'istanza della classe del convertitore allo strumento di creazione RoomDatabase:

Kotlin

val db = Room.databaseBuilder(...)
  .addTypeConverter(exampleConverterInstance)
  .build()

Java

AppDatabase db = Room.databaseBuilder(...)
  .addTypeConverter(exampleConverterInstance)
  .build();

Perché la stanza virtuale non consente riferimenti agli oggetti

Conclusione importante: La stanza non consente i riferimenti a oggetti tra classi di entità. Devi invece richiedere esplicitamente i dati necessari all'app.

La mappatura delle relazioni di un database al rispettivo modello a oggetti è una pratica comune e funziona molto bene sul lato server. Anche quando il programma carica i campi al momento dell'accesso, il server funziona comunque bene.

Tuttavia, sul lato client, questo tipo di caricamento lento non è fattibile perché di solito si verifica nel thread dell'interfaccia utente e l'esecuzione di query sulle informazioni su disco nel thread della UI crea problemi significativi di prestazioni. In genere, il thread dell'interfaccia utente ha circa 16 ms per calcolare e tracciare il layout aggiornato di un'attività, quindi, anche se una query richiede solo 5 ms, è comunque probabile che la tua app non abbia tempo per disegnare il frame, causando problemi visivi evidenti. Il completamento della query potrebbe richiedere ancora più tempo se è in esecuzione una transazione separata in parallelo o se il dispositivo esegue altre attività che richiedono un uso intensivo del disco. Se non utilizzi il caricamento lento, tuttavia, l'app recupera più dati del necessario, creando problemi di consumo di memoria.

In genere, i mapping relazionali agli oggetti lasciano questa decisione agli sviluppatori, in modo che possano fare ciò che è meglio per i casi d'uso della loro app. Gli sviluppatori di solito decidono di condividere il modello tra la loro app e la UI. Tuttavia, questa soluzione non scala bene poiché l'interfaccia utente cambia nel tempo, il modello condiviso crea problemi difficili da prevedere e sottoporre a debug per gli sviluppatori.

Ad esempio, considera una UI che carica un elenco di oggetti Book, dove ogni libro ha un oggetto Author. Inizialmente potresti progettare le tue query in modo da utilizzare il caricamento lento per fare in modo che le istanze di Book recuperino l'autore. Il primo recupero del campo author esegue query nel database. Qualche tempo dopo ti rendi conto che devi mostrare il nome dell'autore anche nell'interfaccia utente dell'app. Puoi accedere a questo nome abbastanza facilmente, come mostrato nel seguente snippet di codice:

Kotlin

authorNameTextView.text = book.author.name

Java

authorNameTextView.setText(book.getAuthor().getName());

Tuttavia, questa modifica apparentemente innocente causa una query sulla tabella Author nel thread principale.

Se esegui query anticipatamente sulle informazioni sull'autore, diventa difficile modificare la modalità di caricamento dei dati se non ti servono più. Ad esempio, se l'interfaccia utente dell'app non deve più mostrare informazioni Author, l'app carica in modo efficace i dati che non vengono più visualizzati, sprecando spazio di memoria prezioso. L'efficienza della tua app si riduce ulteriormente se la classe Author fa riferimento a un'altra tabella, ad esempio Books.

Per fare riferimento a più entità contemporaneamente utilizzando Room, devi creare un POJO che contiene ogni entità e poi scrivere una query che unisce le tabelle corrispondenti. Questo modello ben strutturato, insieme alle solide funzionalità di convalida delle query di Room, consente alla tua app di consumare meno risorse durante il caricamento dei dati, migliorando le prestazioni dell'app e l'esperienza utente.