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

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

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

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

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

Котлин

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

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

Затем вы добавляете аннотацию @TypeConverters к классу AppDatabase , чтобы Room знал об определенном вами классе преобразователя:

Котлин

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

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

Котлин

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

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

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

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

Котлин

@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) {
    ...
  }
}

Затем, помимо объявления класса конвертера в @TypeConverters , используйте метод RoomDatabase.Builder.addTypeConverter() чтобы передать экземпляр класса конвертера строителю RoomDatabase :

Котлин

val db = Room.databaseBuilder(...)
  .addTypeConverter(exampleConverterInstance)
  .build()

Ява

AppDatabase db = Room.databaseBuilder(...)
  .addTypeConverter(exampleConverterInstance)
  .build();

Поймите, почему Room не разрешает ссылки на объекты

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

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

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

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

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

Котлин

authorNameTextView.text = book.author.name

Ява

authorNameTextView.setText(book.getAuthor().getName());

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

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

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