Como referenciar dados complexos usando a Room

A 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 é compatível com referências de objetos.

Usar conversores de tipos

Às vezes, seu app precisa usar um tipo de dado personalizado, e você gostaria de armazenar o valor dele em uma única coluna do banco de dados. Para adicionar esse tipo de compatibilidade com tipos personalizados, forneça um TypeConverter, que converte uma classe personalizada em um tipo conhecido que a Room pode persistir e vice-versa.

Por exemplo, se quisermos persistir instâncias de Date, é possível criar o TypeConverter a seguir para armazenar o carimbo de data/hora Unix equivalente no banco de dados:

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

O exemplo anterior define duas funções: uma que converte um objeto Date em um objeto Long, e outra que realiza a conversão inversa (de Long para Date). Como a Room já sabe como persistir objetos Long, ela pode usar esse conversor para persistir valores do tipo Date.

Em seguida, adicione a anotação @TypeConverters à classe AppDatabase para que a Room possa usar o conversor definido para cada entidade e DAO nesse AppDatabase:

AppDatabase

Kotlin

    @Database(entities = arrayOf(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 esses conversores, é possível usar seus tipos personalizados em outras consultas, da mesma forma que você usaria tipos primitivos, conforme mostrado no seguinte snippet de código:

User

Kotlin

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

Java

    @Entity
    public class User {
        private Date birthday;
    }
    

UserDao

Kotlin

    @Dao
    interface UserDao {
        @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
        fun findUsersBornBetweenDates(from: Date, to: Date): List<User>
    }
    

Java

    @Dao
    public interface UserDao {
        @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
        List<User> findUsersBornBetweenDates(Date from, Date to);
    }
    

Também é possível limitar os @TypeConverters a diferentes escopos, incluindo as entidades individuais, DAOs e métodos DAO. Para ver mais detalhes, consulte a documentação de referência da anotação @TypeConverters.

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

Principal informação: o Room não permite referências de objetos entre classes de entidades. Em vez disso, é necessário solicitar explicitamente os dados que seu 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 à medida que 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 IU criam sérios problemas de desempenho. A linha de execução de IU normalmente tem cerca de 16 ms para calcular e desenhar o layout atualizado de uma atividade. Assim, mesmo que uma consulta demore apenas 5 ms, ainda é provável que seu app fique sem tempo para desenhar o frame, o que causará 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 está executando outras tarefas que ocupam espaço em disco. No entanto, se você não usar o carregamento lento, seu app 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 para os desenvolvedores, para que eles possam fazer o que for melhor para os 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 suas consultas para usar o carregamento lento para que as instâncias de Book recuperem o autor. A primeira recuperação 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 seguinte snippet de código:

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, será difícil mudar a forma como os dados são carregados caso você deixe de precisar deles. Por exemplo, se a IU do app não precisar mais exibir informações de Author, o app carregará dados que não poderão ser exibidos, desperdiçando espaço valioso da memória. A eficiência do app diminui ainda mais se a classe Author fizer referência a outra tabela, como Books.

Para referenciar várias entidades ao mesmo tempo usando a Room, crie um POJO que contenha cada entidade e, então, 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.