Como referenciar dados complexos usando o Room

O Room oferece uma função para a conversão entre tipos primitivos e em caixa, mas não permite referências de objetos entre entidades. Este documento explica como usar conversores de tipo e o motivo por que o Room não oferece suporte a referências de objetos.

Usar conversores de tipos

Às vezes, os apps precisam armazenar um tipo de dados personalizado em uma única coluna do banco de dados. Para que o app ofereça suporte a tipos personalizados, é necessário oferecer conversores de tipo, que são métodos que informam ao Room como executar a conversão entre tipos personalizados e tipos conhecidos, que podem ser persistidos pelo Room. Para identificar os conversores de tipo, use a anotação @TypeConverter.

Suponha que você precise persistir instâncias de Date no banco de dados do Room. O Room não sabe como persistir objetos Date. Portanto, é necessário definir conversores de tipo:

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

Esse exemplo define dois métodos de conversão de tipo: um que converte um objeto Date em um objeto Long e outro que executa a conversão inversa de Long para Date. Como o Room sabe como persistir objetos Long, ele pode usar esses conversores em objetos Date.

Em seguida, adicione a anotação @TypeConverters à classe AppDatabase para informar ao Room a classe de conversor definida:

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

Com os conversores de tipo definidos, é possível usar tipos personalizados nas entidades e DAOs da mesma forma que você usaria tipos primitivos:

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

Nesse exemplo, o Room consegue usar o conversor de tipo definido em todos os lugares, porque você inclui a anotação @TypeConverters em AppDatabase. No entanto, também é possível definir o escopo de conversores de tipo como entidades ou DAOs específicos, incluindo a anotação @TypeConverters nas classes @Entity ou @Dao.

Controlar a inicialização do conversor de tipo

Geralmente, o Room processa a instanciação de conversores de tipo por você. No entanto, às vezes pode ser necessário transmitir outras dependências para as classes de conversores de tipo, fazendo com que você precise que o app controle diretamente a inicialização dos conversores. Nesse caso, adicione a anotação @ProvidedTypeConverter à classe de conversão:

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) {
    ...
  }
}

Em seguida, além de declarar a classe de conversor em @TypeConverters, use o método RoomDatabase.Builder.addTypeConverter() para transmitir uma instância da classe de conversor ao builder RoomDatabase:

Kotlin

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

Java

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

Entender por que o Room não permite referências de objetos

Principal conclusão: o Room não permite referências de objetos entre classes de entidades. Em vez disso, é necessário solicitar explicitamente os dados de que o app precisa.

O mapeamento de relações de um banco de dados para o respectivo modelo de objeto é uma prática comum e funciona muito bem no servidor. Mesmo quando o programa carrega campos quando são acessados, o servidor ainda tem um bom desempenho.

Contudo, no lado do cliente, esse tipo de carregamento lento não é viável, porque geralmente ocorre na linha de execução de IU, e consultas a informações do disco na linha de execução de interface causam sérios problemas de desempenho. A linha de execução de interface normalmente tem cerca de 16ms para calcular e mostrar o layout atualizado de uma atividade. Assim, mesmo que uma consulta demore apenas 5ms, ainda é provável que o app fique sem tempo para mostrar o frame, o que causa falhas visuais significativas. A consulta pode levar ainda mais tempo para ser concluída caso haja uma transação separada em execução paralela ou se o dispositivo estiver executando outras tarefas que ocupam espaço em disco. No entanto, se você não usar o carregamento lento, o app vai buscar mais dados do que o necessário, criando problemas de consumo de memória.

Os mapeamentos relacionais de objetos normalmente deixam essa decisão aos desenvolvedores, para que eles possam fazer o que for melhor nos casos de uso do app. Geralmente, os desenvolvedores decidem compartilhar o modelo entre o app e a IU. No entanto, essa solução não é muito adequada porque, à medida que a IU muda ao longo do tempo, o modelo compartilhado cria problemas difíceis de serem previstos e depurados pelos desenvolvedores.

Por exemplo, considere uma IU que carrega uma lista de objetos Book, com cada livro tendo um objeto Author. Você pode, inicialmente, projetar as consultas para usar o carregamento lento de forma que as instâncias de Book acessem o autor. O primeiro acesso do campo author consulta o banco de dados. Algum tempo depois, você percebe que também precisa exibir o nome do autor na IU do app. É possível acessar esse nome com facilidade, conforme mostrado no snippet de código abaixo:

Kotlin

authorNameTextView.text = book.author.name

Java

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

No entanto, essa mudança aparentemente inocente faz com que a tabela Author seja consultada na linha de execução principal.

Se as informações do autor forem consultadas antecipadamente, vai ser difícil mudar a forma como os dados são carregados caso você não precise mais deles. Por exemplo, se a IU do app não precisar mais exibir informações de Author, o app carrega dados que não podem ser exibidos, desperdiçando espaço valioso da memória. A eficiência do app diminui ainda mais se a classe Author referenciar outra tabela, como Books.

Para referenciar várias entidades ao mesmo tempo usando o Room, crie um POJO que contenha cada entidade e programe uma consulta que mescle as tabelas correspondentes. Esse modelo bem estruturado, combinado às funções robustas de validação de consultas do Room, permite que o app consuma menos recursos ao carregar dados, melhorando o desempenho do app e a experiência do usuário.