使用 Room 引用复杂数据

Room 提供了在基本类型和包装类型之间转换的功能,但不允许实体之间存在对象引用。本文档说明了如何使用类型转换器以及 Room 为什么不支持对象引用。

使用类型转换器

有时,您需要您的应用将自定义数据类型存储在一个数据库列中。您可以通过提供类型转换器来支持自定义类型,类型转换器是指示 Room 如何将自定义类型转换为 Room 可以持久化的已知类型的方法,以及如何进行反向转换。您可以使用@TypeConverter注释来识别类型转换器。

假设您需要在 Room 数据库中持久化Date的实例。Room 不知道如何持久化Date对象,因此您需要定义类型转换器

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

此示例定义了两个类型转换器方法:一个将Date对象转换为Long对象,另一个执行从LongDate的反向转换。由于 Room 知道如何持久化Long对象,因此它可以使用这些转换器来持久化Date对象。

接下来,您将@TypeConverters注释添加到AppDatabase类中,以便 Room 了解您已定义的转换器类

Kotlin

@Database(entities = [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();
}

定义了这些类型转换器后,您可以在实体和 DAO 中像使用基本类型一样使用自定义类型

Kotlin

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

@Dao
interface UserDao {
  @Query("SELECT * FROM user WHERE birthday = :targetDate")
  fun findUsersBornOnDate(targetDate: Date): List<User>
}

Java

@Entity
public class User {
  private Date birthday;
}

@Dao
public interface UserDao {
  @Query("SELECT * FROM user WHERE birthday = :targetDate")
  List<User> findUsersBornOnDate(Date targetDate);
}

在此示例中,Room 可以随处使用定义的类型转换器,因为您使用@TypeConverters注释了AppDatabase。但是,您还可以通过使用@TypeConverters注释@Entity@Dao类,将类型转换器范围限定到特定的实体或 DAO。

控制类型转换器初始化

通常,Room 会为您处理类型转换器的实例化。但是,有时您可能需要将其他依赖项传递给类型转换器类,这意味着您需要您的应用直接控制类型转换器的初始化。在这种情况下,请使用@ProvidedTypeConverter注释转换器类

Kotlin

@ProvidedTypeConverter
class ExampleConverter {
  @TypeConverter
  fun StringToExample(string: String?): ExampleType? {
    ...
  }

  @TypeConverter
  fun ExampleToString(example: ExampleType?): String? {
    ...
  }
}

Java

@ProvidedTypeConverter
public class ExampleConverter {
  @TypeConverter
  public Example StringToExample(String string) {
    ...
  }

  @TypeConverter
  public String ExampleToString(Example example) {
    ...
  }
}

然后,除了在@TypeConverters中声明转换器类之外,还可以使用RoomDatabase.Builder.addTypeConverter()方法将转换器类的实例传递给RoomDatabase构建器

Kotlin

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

Java

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

了解 Room 为什么不允许对象引用

关键要点: Room 不允许实体类之间进行对象引用。相反,您必须显式请求应用程序所需的数据。

将数据库中的关系映射到相应的对象模型是一种常见的做法,并且在服务器端运行良好。即使程序在访问时加载字段,服务器仍然可以很好地运行。

但是,在客户端,这种类型的延迟加载是不可行的,因为它通常发生在 UI 线程上,并且在 UI 线程上查询磁盘上的信息会导致严重的性能问题。UI 线程通常大约有 16 毫秒的时间来计算和绘制活动更新的布局,因此即使查询只需要 5 毫秒,您的应用程序也可能仍然没有足够的时间来绘制帧,从而导致明显的视觉故障。如果同时运行单独的事务,或者设备正在运行其他磁盘密集型任务,则查询可能需要更长时间才能完成。但是,如果不使用延迟加载,您的应用程序将获取比需要更多的数据,从而导致内存消耗问题。

对象关系映射通常将此决定留给开发人员,以便他们可以为其应用程序的使用案例做出最佳选择。开发人员通常决定在他们的应用程序和 UI 之间共享模型。但是,此解决方案的可扩展性不佳,因为随着时间的推移,UI 发生变化,共享模型会产生开发人员难以预测和调试的问题。

例如,考虑一个加载 Book 对象列表的 UI,每个书籍都有一个 Author 对象。您可能最初设计查询以使用延迟加载,以便 Book 实例检索作者。第一次检索 author 字段会查询数据库。一段时间后,您意识到您需要在应用程序的 UI 中显示作者姓名。您可以像以下代码片段中所示的那样轻松访问此名称

Kotlin

authorNameTextView.text = book.author.name

Java

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

但是,这种看似无害的更改会导致在主线程上查询 Author 表。

如果您提前查询作者信息,那么如果不再需要这些数据,则很难更改数据加载方式。例如,如果您的应用程序的 UI 不再需要显示 Author 信息,则您的应用程序实际上加载了不再显示的数据,浪费了宝贵的内存空间。如果 Author 类引用另一个表(例如 Books),则应用程序的效率会进一步降低。

要使用 Room 同时引用多个实体,您可以创建一个包含每个实体的 POJO,然后编写一个连接相应表的查询。这种结构良好的模型,结合 Room 强大的查询验证功能,可以让您的应用程序在加载数据时消耗更少的资源,从而提高应用程序的性能和用户体验。