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
对象,另一种执行从 Long
到 Date
的逆向转换。由于 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); }
在此示例中,由于您使用 @TypeConverters
注解了 AppDatabase
,Room 可以在任何地方使用已定义的类型转换器。但是,您也可以通过使用 @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 不允许实体类之间存在对象引用。您必须显式请求应用所需的数据。
将数据库中的关系映射到相应的对象模型是一种常见做法,在服务器端效果很好。即使程序在访问字段时才加载它们,服务器仍然能良好运行。
但是,在客户端,这种**延迟加载 (lazy loading)** 不可行,因为它通常发生在 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 强大的查询验证功能,使您的应用在加载数据时消耗更少的资源,从而提高应用的性能和用户体验。