Room 提供原生类型和装箱类型的类型转换,但是不允许实体间的对象引用。本文说明了如何使用转换器以及Room
为什么不支持对象引用。
使用类型转换器
有时候,你想在数据库某列存储自定义数据类型。为了支持这种自定义类型,提供一个 TypeConverter
,可以在定义类型和已知类型之间转换以便Room
保存。
例如,如果我们想保存Date
,可以编写如下TypeConverter
在数据库中存储Unix
的等效时间戳:
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time?.toLong()
}
}
前面的示例中定义了两个函数,一个将Date
转为Long
,一个将Long
转为Date
。Room
知道如何保存 Long
,所以可以利用转换器保存Date
。
下一步,在 数据库类上添加@TypeConverters 注解以便将转换器应用于你所定义的实体和DAO
中:
@Database(entities = arrayOf(User::class), version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
使用这些转换器,你可以在查询中像使用原生类型一样使用自定义类型:
User
@Entity
data class User(private val birthday: Date?)
UserDao
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
fun findUsersBornBetweenDates(from: Date, to: Date): List<User>
}
还可以限制@TypeConverters
使用范围,包括单个实体,DAO
和DAO
方法。详情请参看@TypeConverters
理解 Room 为什么不允许对象间引用
关键点:Room 不允许多个实体的引用,相反,你必须在 app 中明确请求你需要的数据。
将数据库中的关系映射到对象模型是一种比较常见的作法,并且这在服务器端运行良好,甚至在程序访问字段时才加载它们,仍然表现良好。
然而,在客户端,这种懒加载模式因经常运行在UI线程而变得不可行。在UI线程查询磁盘上的信息会引发严重的性能问题。UI线程大概有16ms来计算和绘制视图,所以即使查询只占用其中的5ms,仍然可能导致没有足够的时间来绘制视图造成掉帧。 如果有一个单独的事务在并行运行,或者磁盘正在做其他密集型运算,那么查询工作将会消耗更多的时间。如果不使用惰性加载,那么你将获取到很多用不着的数据,造成内存消耗问题。
开发者知道怎样结合app用例使对象关系映射运行得更好,通常开发者在app和UI之间共享模型,但是这种方式的扩展性不是很好,因为随着UI的变化,共享模型会产生开发者难以预料和调试的问题。
举个栗子,假设某个UI会加载书本对象的集合,每本书都有一作者对象。最初设计中你可能会使用惰性加载来书本实例检索作者。第一次检索作者字段会查询数据库,过了一段时间,你意识到你需要将作者名称显示在UI上,那么你可以使用下面的代码很方便的访问
authorNameTextView.text = book.author.name
然而,这种看似无害的更改会导致在主线程上查询作者表。
如果提前查询作者信息,如果某一天不再需要这些数据,更改这些数据的加载方式是很困难的。例如,如果你的app不需要显示作者信息,app加载的这些用不着的数据就是浪费内存空间。如果作者类引用另一个类,如书籍类,app的运行效率将进一步降低。
要使用Room
同时引用多个实体,你需要写一个连接表查询而不是创建一个包含每一个实体的POJO
,这种结构良好的模型,结合Room
强大的查询验证功能,允许您的应用程序在加载数据时消耗更少的资源,从而提高应用程序的性能和用户体验。