Ссылка на сложные данные с помощью Room

Room может преобразовывать примитивные и упакованные типы, но не допускает ссылок на объекты между сущностями. Узнайте, как использовать преобразователи типов и почему Room не поддерживает ссылки на объекты.

Используйте преобразователи типов.

Иногда вашему приложению необходимо хранить данные пользовательского типа в одном столбце базы данных. Поддержка пользовательских типов осуществляется с помощью преобразователей типов . Это функции, которые указывают Room, как преобразовывать пользовательские типы в известные типы и обратно, которые Room может сохранять. Преобразователи типов идентифицируются с помощью аннотации @ColumnTypeConverter .

Предположим, вам нужно сохранять экземпляры типа Date в вашей базе данных Room. Room не может изначально сохранять объекты Date , поэтому вам необходимо определить преобразователи типов:

object Converters {
    @ColumnTypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @ColumnTypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time
    }
}

В этом примере определены две функции преобразования типов: одна преобразует объект Date в объект Long , а другая преобразует объект Long обратно в объект Date . Поскольку Room может сохранять объекты Long , он может использовать эти функции преобразования для сохранения объектов Date .

Далее добавьте аннотацию @ColumnTypeConverters к классу AppDatabase , чтобы Room мог использовать определенный вами класс конвертера:

@Database(entities = [User::class], version = 1)
@ColumnTypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

Определив эти преобразователи типов, вы можете использовать свой пользовательский тип в сущностях и DAO так же, как и примитивные типы:

@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>
}

Поскольку в этом примере вы аннотировали AppDatabase с помощью @ColumnTypeConverters , Room может использовать определенный преобразователь типов повсюду. Чтобы ограничить применение преобразователей типов определенными сущностями или DAO, аннотируйте ваши классы @Entity или @Dao с помощью @ColumnTypeConverters .

Инициализация преобразователя типа управления

Обычно Room автоматически создает экземпляры преобразователей типов. Однако, если вам необходимо передать дополнительные зависимости классам преобразователей типов, ваше приложение должно напрямую контролировать их инициализацию. В этом случае аннотируйте свой класс преобразователя с помощью @ProvidedColumnTypeConverter :

@ProvidedColumnTypeConverter
class ExampleConverter {
    @ColumnTypeConverter
    fun stringToExample(string: String?): ExampleType? {
        return string?.let { ExampleType() }
    }

    @ColumnTypeConverter
    fun exampleToString(example: ExampleType?): String? {
        return example?.toString()
    }
}

Помимо объявления класса конвертера в @ColumnTypeConverters , используйте функцию RoomDatabase.Builder.addColumnTypeConverter для передачи экземпляра вашего класса конвертера в построитель RoomDatabase :

val db = Room.databaseBuilder<MyDatabase>(applicationContext, "database-name")
    .addColumnTypeConverter(exampleConverterInstance)
    .build()

Разберитесь, почему Room не допускает ссылки на объекты.

Главный вывод: Room запрещает ссылки на объекты между классами сущностей. Вместо этого необходимо явно запрашивать данные, необходимые вашему приложению.

Сопоставление связей из базы данных с соответствующей объектной моделью — распространенная практика, которая отлично работает на стороне сервера. Даже когда программа загружает свойства по мере обращения к ним, сервер все равно демонстрирует высокую производительность.

Однако на стороне клиента такой тип отложенной загрузки нецелесообразен, поскольку обычно он происходит в потоке пользовательского интерфейса, а запрос информации с диска в этом потоке создает значительные проблемы с производительностью. Поток пользовательского интерфейса обычно имеет около 16 мс на вычисление и отрисовку обновленного макета активности, поэтому даже если запрос занимает всего 5 мс, все равно существует вероятность того, что вашему приложению не хватит времени для отрисовки кадра, что приведет к заметным визуальным сбоям. Запрос может занять еще больше времени, если параллельно выполняется отдельная транзакция или если на устройстве выполняются другие ресурсоемкие задачи, связанные с диском. Однако, если вы не используете отложенную загрузку, ваше приложение будет получать больше данных, чем ему нужно, что создаст проблемы с потреблением памяти.

Объектно-реляционные отображения обычно оставляют это решение на усмотрение разработчиков, чтобы они могли выбрать наиболее подходящий вариант для конкретных сценариев использования своего приложения. Разработчики обычно решают использовать одну модель как для приложения, так и для пользовательского интерфейса. Однако это решение плохо масштабируется, поскольку по мере изменения пользовательского интерфейса с течением времени общая модель создает проблемы, которые разработчикам трудно предвидеть и отлаживать.

Например, рассмотрим пользовательский интерфейс, который загружает список объектов Book , причем каждая книга имеет объект Author . Изначально вы можете настроить запросы на отложенную загрузку, чтобы экземпляры Book получали имя автора. Первое получение свойства author выполняется путем запроса к базе данных. Через некоторое время вы понимаете, что вам также необходимо отображать имя автора в пользовательском интерфейсе вашего приложения. Вы можете получить доступ к этому имени, как показано в следующем фрагменте кода:

Text(text = book.author.name)

Однако это, казалось бы, безобидное изменение приводит к тому, что запрос к таблице Author выполняется в основном потоке.

Если вы запрашиваете информацию об авторе заранее, но она вам не нужна, изменить способ загрузки данных будет сложно. Например, если интерфейс вашего приложения больше не нуждается в отображении информации Author , приложение фактически загружает данные, которые оно не отображает, тратя ценное пространство памяти. Эффективность вашего приложения снижается еще больше, если класс Author ссылается на другую таблицу, например, Books .

Для одновременного обращения к нескольким сущностям с помощью Room создайте объект данных, содержащий каждую сущность, а затем напишите запрос, который объединяет соответствующие таблицы. Эта хорошо структурированная модель в сочетании с мощными возможностями проверки запросов Room позволяет вашему приложению потреблять меньше ресурсов при загрузке данных, улучшая производительность и пользовательский опыт.