Room 在 SQLite 上提供了一个抽象层,以便在利用 SQLite 强大功能的同时,能够流畅地访问数据库。
Room 包含三个主要组件:
- 数据库类:保存数据库并作为应用持久性数据底层连接的主要访问点
- 数据实体:表示数据库中的表
- 数据访问对象( Dao ):提供用于查询、更新、插入和删除数据库中数据的方法
使用的依赖:
plugins {
...
id 'kotlin-kapt'
}
def room_version = "2.4.3"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation "androidx.activity:activity-ktx:1.5.1"
implementation "com.google.code.gson:gson:2.9.0"
数据实体
每个 Room 实体定义为带有 @Entity 注解的类,Room 实体包含数据库中相应表中的每一列的字段,包括构成主键的一个或多个列。每个 Room 实体需要定义一个主键,用于唯一标识相应数据库表中的每一行,可以用注解 @PrimaryKey,如果需要分配自动 ID,可将 @PrimaryKey 的 autoGenerate 属性设为 true。
@Entity
data class User(
@PrimaryKey(autoGenerate = true) val id: Int,
val name: String,
val age: Int
)
默认情况下,Room 将类名称用作数据库表名称。如果想要表具有不同的名称,可以设置 @Entity 注解的 tableName 属性。同样,Room 默认使用字段名作为数据库中的列名称。如果想要列具有不同的名称,请将 @ColumnInfo 注解添加到该字段并设置 name 属性,SQLite 中的表和列名称不区分大小写。
@Entity(tableName = "people")
data class User(
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val name: String,
@ColumnInfo(name = "actual_age") val age: Int
)
如果需要通过多个列的组合对实体实例进行唯一标识,则可以通过 @Entity 的 primaryKeys 属性定义一个复合主键。默认情况下,Room 会为实体中定义的每个字段创建一个列,如果有你不想保留的字段,则可以使用 @Ignore 注解忽略字段。
@Entity(primaryKeys = ["id", "name"])
data class User(
val id: Int,
val name: String,
val age: Int,
@Ignore val address: String
)
可以将数据库中的某些列编入索引,以加快查询速度。在 @Entity 注解中添加 indices 属性,列出要在索引或复合索引中包含的列名称即可。有时,数据库中的某些字段或字段组规定是唯一的,可以通过将 Index 的 unique 属性设为 true
@Entity(indices = [Index(value = ["name", "address"], unique = true)])
data class User(
@PrimaryKey val id: Int,
val name: String,
val age: Int,
val address: String
)
数据访问对象 DAO
可以将每个 DAO 定义为一个接口或抽象类,添加注解 @Dao,DAO 不具有属性,但它们定义了一个或多个方法,可用于与应用数据库中的数据进行交互。
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg user: User)
@Delete
fun delete(user: User)
@Update
fun updateUsers(vararg user: User)
@Query("SELECT * FROM user")
fun getAllUsers(): List<User>
}
借助 @Update 和 @Delete 注释,可以定义更新和删除数据库表中特定行的方法,Room 使用主键将传递的实体实例与数据库中的行进行匹配,如果没有相同主键的行,Room 不会进行任何更改。
有时,我们只需要返回要查询的表中的列的子集,我们可以将一组结果列映射到返回的对象。
比如,我们的数据实体是这样的
@Entity
data class User(
@PrimaryKey val id: Int,
val name: String,
val age: Int,
val address: String
)
而我们只希望返回 name 和 address ,此时可以新建一个类,然后从查询方法返回该对象,注意属性名要相对应。
data class UserInfo(
val name: String,
val address: String
)
@Query("SELECT name,address FROM user")
fun loadUserInfo(): List<UserInfo>
DAO 方法也可以接收参数,以便执行过滤操作。
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadOldUsers(minAge: Int): List<User>
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
fun loadRangeAgeUsers(minAge: Int, maxAge: Int): List<User>
@Query("SELECT * FROM user WHERE name LIKE :searchName")
fun loadUsersByName(searchName: String): List<User>
@Query("SELECT * FROM user WHERE address IN (:searchAddress)")
fun loadUsersFromAddress(searchAddress: List<String>): List<User>
多表查询,可以使用 JOIN 子句来引用多个表。
@Query(
"SELECT * FROM book " +
"INNER JOIN loan ON loan.book_id = book.id " +
"INNER JOIN user ON user.id = loan.user_id " +
"WHERE user.name LIKE :userName"
)
fun loadBookBorrowedByName(userName: String): List<Book>
@Query("SELECT user.name AS userName,book.name AS bookName FROM user,book WHERE user.id = book.user_id ")
fun loadUserBook(): List<UserBook>
@Entity
data class Loan(
val book_id: Int,
val user_id: Int
)
@Entity
data class Book(
@PrimaryKey val id: Int,
val name: String,
val user_id: Int
)
@Entity
data class User(
@PrimaryKey val id: Int,
val name: String,
val age: Int,
val address: String
)
异步查询
Room 不允许在主线程上访问数据库,必须将 DAO 查询设为异步
单次查询:仅执行一次并在执行时获取数据快照的数据库操作
@Query("SELECT * FROM user")
suspend fun getAllUsers(): List<User>
可观察查询:在查询引用的任何表发生更改时发出新值的读取操作,只要对表中的任何行进行更新,无论该行是否在结果集中,查询都会重新运行。
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadOldUsers(minAge: Int): Flow<List<User>>
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
fun loadRangeAgeUsers(minAge: Int, maxAge: Int): Flow<List<User>>
数据库类
定义数据库配置,并作为应用对持久性数据的主要访问点,数据库类需要满足以下条件:
- 必须带有 @Database 注解,该注解包含列出所有与数据库关联的数据实体的 entities 数组
- 必须是一个抽象类,用于扩展 RoomDatabase
- 必须定义一个具有零参数的抽象方法,并返回 DAO 类的实例
每个 RoomDatabase 实例的成本很高,在实例化 AppDatabase 对象时应遵循单例设计模式。
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
val instance = INSTANCE
instance?.let { return it }
synchronized(this) {
val db = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"database_name"
).build()
INSTANCE = db
return db
}
}
}
}
然后在协程里调用执行
lifecycleScope.launch(Dispatchers.IO) {
val users = AppDatabase.getDatabase(this@MainActivity).userDao().getAllUsers()
}
lifecycleScope.launch(Dispatchers.IO) {
AppDatabase.getDatabase(this@MainActivity).userDao()
.loadRangeAgeUsers(MIN_AGE, MAX_AGE).collect {
Log.i(TAG, "users: $it")
}
}
引用复杂数据
Room 提供了在基元类型和盒装类型之间进行转换的功能,但不允许在实体之间进行对象引用,此时,我们可以借助类型转换器,告知 Room 如何将自定义类型与 Room 可以保留的已知类型相互转换,使用 @TypeConverter 注解来识别类型转换器。
例如,现在我们的 User 表中需要有个 Friend 对象,Friend 是自定义类型
data class Friend(
val friendName: String,
val friendAge: Int
)
class Converters {
@TypeConverter
fun friendToString(friend: Friend): String {
return Gson().toJson(friend)
}
@TypeConverter
fun stringToFriend(string: String): Friend {
val type = object : TypeToken<Friend>() {}.type
return Gson().fromJson(string, type)
}
}
在 AppDatabase 类上添加 @TypeConverters 注解,让 Room 知道你定义的转换器类,然后就可以在实体和 DAO 中使用自定义类型,就像使用基元类型一样。
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {...}
数据库迁移
比如,现在只有 user 表,需要增加 Loan 和 Book 表,需要在 AppDatabase 做手动迁移
@Database(entities = [User::class, Book::class, Loan::class], version = 2)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun bookDao(): BookDao
abstract fun loanDao(): LoanDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"CREATE TABLE `Book` (`id` INTEGER NOT NULL,`name` TEXT NOT NULL," +
"`user_id` INTEGER NOT NULL,PRIMARY KEY(`id`))"
)
database.execSQL(
"CREATE TABLE `Loan` (`id` INTEGER NOT NULL,`book_id` INTEGER NOT NULL," +
"`user_id` INTEGER NOT NULL,PRIMARY KEY(`id`))"
)
}
}
fun getDatabase(context: Context): AppDatabase {
val instance = INSTANCE
instance?.let { return it }
synchronized(this) {
val db = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"database_name"
).addMigrations(MIGRATION_1_2).build()
INSTANCE = db
return db
}
}
}
}
@Entity
data class Loan(
@PrimaryKey val id: Int,
val book_id: Int,
val user_id: Int
)
@Entity
data class Book(
@PrimaryKey val id: Int,
val name: String,
val user_id: Int
)
比如,我现在需要往 User 表中添加字段 gender ,此时在 AppDatabase 做手动迁移操作,然后在实体类添加属性 gender 就OK啦
@Database(entities = [User::class, Book::class, Loan::class], version = 3)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun bookDao(): BookDao
abstract fun loanDao(): LoanDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE user ADD COLUMN gender TEXT NOT NULL DEFAULT 'male'")
}
}
fun getDatabase(context: Context): AppDatabase {
val instance = INSTANCE
instance?.let { return it }
synchronized(this) {
val db = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"database_name"
).addMigrations(MIGRATION_2_3).build()
INSTANCE = db
return db
}
}
}
}
SQLite 具有以下五种数据类型:
- NULL:空值
- INTEGER:带符号的整型
- REAL:浮点数字
- TEXT:字符串文本
- BLOB:二进制对象