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.