Android Jetpack - Room

243 阅读2分钟

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 具有以下五种数据类型:

  1. NULL:空值
  2. INTEGER:带符号的整型
  3. REAL:浮点数字
  4. TEXT:字符串文本
  5. BLOB:二进制对象