如果您的应用处理大量结构化数据,那么在本地保留这些数据会带来许多好处。最常见的使用场景是缓存相关的数据,这样一来,当设备无法访问网络时,您仍然可以在离线状态下浏览该内容。
Room 持久性库在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。
设置 Room 2.x
如需在应用中使用 Room 2.x,请将以下依赖项添加到应用的 build.gradle 文件:
dependencies {
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
// To use Kotlin Symbol Processing (KSP)
// ksp("androidx.room:room-compiler:$room_version")
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$room_version")
// optional - RxJava2 support for Room
implementation("androidx.room:room-rxjava2:$room_version")
// optional - Guava support for Room, including Optional and ListenableFuture
implementation("androidx.room:room-guava:$room_version")
// optional - Test helpers
testImplementation("androidx.room:room-testing:$room_version")
}
主要组件
Room 有三个主要组件:
- 数据库类 ,用于保存数据库并作为应用持久性数据底层连接的主要访问点。
- 数据实体 ,用于表示应用的数据库中的表。
- 数据访问对象 (DAO) ,为您的应用提供在数据库中查询、更新、插入和删除数据的方法。
图 1 说明了 Room 的不同组件之间的关系。
实现示例
// Entity
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
// DAO
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
// Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// Usage
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
使用实体定义数据
每个 Room 实体都表示数据库中的一个表。您可以使用 @Entity 注解将每个实体定义为一个
类。
@Entity(tableName = "users")
data class User (
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore val picture: Bitmap? = null
)
- 自定义表名和列名:默认情况下,Room 将类名用作
表名,并将属性名称用作列名。如需自定义这些名称,请使用
tableName属性(位于@Entity中)和@ColumnInfo(name = "...")注解。 - 主键:如需定义主键,请使用
@PrimaryKey。对于 复合键,请使用primaryKeys属性的@Entity:@Entity(primaryKeys = ["firstName", "lastName"])。 - 忽略字段:如需阻止字段被保留,请使用
@Ignore。
类型转换器
有时,您需要将自定义类型(例如 Date)存储在单个列中。
提供 @TypeConverter 方法,以将自定义类型与类型
Room 可以保留的类型相互转换。
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
}
// Register in your Database class
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
使用 DAO 访问数据
DAO 定义了用于数据库交互的方法。使用 @Dao 为接口或抽象
类添加注解。
便捷方法
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Update
fun updateUsers(vararg users: User)
@Delete
fun deleteUsers(vararg users: User)
}
- 插入:insert 方法可以返回一个
Long,表示插入的行 ID;也可以返回一个List<Long>,其中包含所有插入行的 ID。 - 更新或删除:update 或 delete 方法可以返回一个
Int,表示受影响的行数。
查询方法
使用 @Query 为方法添加注解,以编写 SQL 语句。Room 会在编译时验证查询。
@Dao
interface UserDao {
// Simple query
@Query("SELECT * FROM user")
fun loadAllUsers(): Array<User>
// Return a subset of columns using a POJO or tuple
@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>
// Pass parameters
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
// Collection of parameters
@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>
// Join tables
@Query("SELECT * FROM book INNER JOIN user ON user.id = book.user_id WHERE user.name = :userName")
fun findBooksBorrowedByName(userName: String): List<Book>
}
多重映射返回值类型
在 Room 2.4 及更高版本中,查询方法可以使用 Map 类型直接返回多重映射:
@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>
异步 DAO 查询
为避免界面冻结,数据库查询无法在主线程上运行。使用以下集成之一,使查询异步运行:
Kotlin 协程和 Flow
需要 room-ktx 依赖项。
@Dao
interface UserDao {
// One-shot async query
@Insert
suspend fun insertUsers(vararg users: User)
// Observable query using Flow
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flow<User>
}
Java 与 RxJava
需要 room-rxjava2 或 room-rxjava3。
@Dao
interface UserDao {
@Insert
fun insertUsers(users: List<User>): Completable
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flowable<User>
}
Java 与 LiveData 和 Guava
需要 room-guava 才能使用 ListenableFuture。
@Dao
interface UserDao {
// LiveData for observable queries
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): LiveData<User>
// Guava ListenableFuture for one-shot queries
@Insert
fun insertUsers(users: List<User>): ListenableFuture<Integer>
}
在 Room 2.x 中定义关系
为防止在界面线程上延迟加载,您无法在实体之间使用直接对象引用。而是使用带有 @Relation 的中间
数据类定义关系。
一对一
每个用户只有一个库。
@Entity
data class User(@PrimaryKey val userId: Long, val name: String)
@Entity
data class Library(@PrimaryKey val libraryId: Long, val userOwnerId: Long)
// Intermediate class
data class UserAndLibrary(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userOwnerId"
)
val library: Library
)
// DAO Query
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
一对多
每个用户可以有多个播放列表。
@Entity
data class Playlist(@PrimaryKey val playlistId: Long, val userCreatorId: Long)
data class UserWithPlaylists(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<Playlist>
)
多对多
播放列表可以包含多首歌曲,而歌曲可以位于多个播放列表中。需要一个联结表。
@Entity
data class Song(@PrimaryKey val songId: Long, val songName: String)
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(val playlistId: Long, val songId: Long)
data class PlaylistWithSongs(
@Embedded val playlist: Playlist,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class)
)
val songs: List<Song>
)
嵌套关系
查询用户、用户的播放列表以及这些播放列表中的所有歌曲。
data class UserWithPlaylistsAndSongs(
@Embedded val user: User,
@Relation(
entity = Playlist::class,
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
)
数据库管理
本部分介绍了管理 Room 数据库的各个方面,包括数据库视图、预填充数据和数据库迁移。
数据库视图
将复杂查询封装到使用 @DatabaseView 注解的类中。
@DatabaseView("SELECT user.id, user.name, department.name AS departmentName FROM user INNER JOIN department ON user.departmentId = department.id")
data class UserDetail(val id: Long, val name: String, val departmentName: String)
// Register in Database class
@Database(entities = [User::class], views = [UserDetail::class], version = 1)
abstract class AppDatabase : RoomDatabase() { ... }
预填充数据库
在初始化时从资源文件或文件系统填充数据库。
Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
.createFromAsset("database/myapp.db")
.build()
迁移
更改架构时,请递增数据库版本并定义
Migration对象。
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name")
.addMigrations(MIGRATION_1_2)
.build()
- 自动迁移:如果您使用的是 Room 2.4.0 或更高版本,则可以使用
@AutoMigration自动迁移基本架构更改。这要求您在数据库配置中将exportSchema设置为true:@Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)])。 - 破坏性回退:如果允许在迁移路径
缺失的情况下丢失数据,请在构建
数据库时调用
.fallbackToDestructiveMigration。
测试迁移
如需验证迁移,请使用 room-testing 工件中的 MigrationTestHelper。如需支持此功能,请确保在 build.gradle 配置中导出架构。
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test
fun migrate1To2() {
var db = helper.createDatabase("test-db", 1).apply {
execSQL("INSERT INTO User VALUES (1, 'John')")
close()
}
db = helper.runMigrationsAndValidate("test-db", 2, true, MIGRATION_1_2)
// Verify data was migrated correctly
}
}
从 SQLite 迁移到 Room
如需将应用从 SQLite 迁移到 Room,请完成以下步骤:
- 更新依赖项 以包含 Room。
- 使用
@Entity、@PrimaryKey和@ColumnInfo为模型类添加注解 。 - 创建 DAO 以替换辅助查询方法。
- 创建 RoomDatabase 类 ,引用实体和 DAO。 递增版本号。
- 定义空迁移路径,因为架构不会更改,只会更改
框架:
kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} } - 更新实例化 ,以使用带有迁移路径的
Room.databaseBuilder。