A Room pode converter tipos primitivos e em caixa, mas não permite referências de objetos entre entidades. Saiba como usar conversores de tipo e o motivo por que a 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. Essas são funções que informam à Room como executar a conversão entre tipos personalizados e tipos conhecidos, que podem ser persistidos pela Room. Para identificar os conversores de tipo, use a
@ColumnTypeConverter anotação.
Suponha que você precise persistir instâncias de Date em
seu banco de dados do Room. A Room não pode persistir objetos Date de forma nativa. Portanto, é necessário definir conversores de tipo:
object Converters { @ColumnTypeConverter fun fromTimestamp(value: Long?): Date? { return value?.let { Date(it) } } @ColumnTypeConverter fun dateToTimestamp(date: Date?): Long? { return date?.time } }
Este exemplo define duas funções de conversor de tipo: uma que converte um objeto Date em um objeto Long e outra que converte um objeto Long de volta em um objeto Date. Como a Room pode persistir objetos Long, ela pode usar esses conversores em objetos Date.
Em seguida, adicione a anotação @ColumnTypeConverters
à classe AppDatabase para que a Room possa usar a classe de conversor
que você definiu:
@Database(entities = [User::class], version = 1) @ColumnTypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun 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:
@Entity data class User( @PrimaryKey val id: Long, val name: String, val birthday: Date? ) @Dao interface UserDao { @Query("SELECT * FROM user WHERE birthday = :targetDate") suspend fun findUsersBornOnDate(targetDate: Date): List<User> }
Como você anotou AppDatabase com @ColumnTypeConverters neste exemplo, a Room pode usar o conversor de tipo definido em todos os lugares. Para definir o escopo de conversores de tipo como entidades ou DAOs específicos, inclua a anotação @ColumnTypeConverters nas classes @Entity ou @Dao.
Controlar a inicialização do conversor de tipo
Geralmente, a Room instancia conversores de tipo por você. No entanto, se você precisar transmitir outras dependências para as classes de conversores de tipo, o app precisará controlar diretamente a inicialização delas. Nesse caso, anote a classe do conversor
com @ProvidedColumnTypeConverter:
@ProvidedColumnTypeConverter class ExampleConverter { @ColumnTypeConverter fun stringToExample(string: String?): ExampleType? { return string?.let { ExampleType() } } @ColumnTypeConverter fun exampleToString(example: ExampleType?): String? { return example?.toString() } }
Além de declarar a classe do conversor em @ColumnTypeConverters, use
a função RoomDatabase.Builder.addColumnTypeConverter para transmitir uma
instância da classe do conversor ao builder RoomDatabase:
val db = Room.databaseBuilder<MyDatabase>(applicationContext, "database-name") .addColumnTypeConverter(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 propriedades à medida que elas são acessadas, 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. A primeira recuperação da propriedade author consulta o banco de dados. Algum tempo depois, você percebe que também precisa mostrar o nome do autor na interface do app. É possível acessar esse nome, conforme mostrado no snippet de código abaixo:
Text(text = book.author.name)
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, mas você não precisar delas, será difícil mudar a forma como os dados são carregados. Por exemplo, se a interface do app não precisar mais mostrar informações de Author, o app carregará dados que não serão mostrados, desperdiçando espaço valioso da memória. A eficiência do app diminui ainda mais se a classe Author referencia outra tabela, por exemplo, Books.
Para referenciar várias entidades ao mesmo tempo usando a Room, crie um objeto de dados que contenha cada entidade e, em seguida, grave uma consulta que mescle as tabelas correspondentes. Esse modelo bem estruturado, combinado às funções robustas de validação de consultas da Room, permite que seu app consuma menos recursos ao carregar dados, melhorando o desempenho do app e a experiência do usuário.