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:
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time?.toLong()
}
}
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:
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
@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:
@Entity
data class User(private val birthday: Date?)
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE birthday = :targetDate")
fun findUsersBornOnDate(targetDate: Date): List<User>
}
@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:
@ProvidedTypeConverter
class ExampleConverter {
@TypeConverter
fun StringToExample(string: String?): ExampleType? {
...
}
@TypeConverter
fun ExampleToString(example: ExampleType?): String? {
...
}
}
@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
:
val db = Room.databaseBuilder(...)
.addTypeConverter(exampleConverterInstance)
.build()
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:
authorNameTextView.text = book.author.name
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.