Room 使用解析(2.4.2 版本)

1、Room 简介

  • 使用 Jetpack Room 将数据库保存到本地数据库,处理大量结构化数据的应用可极大地受益于在本地保留这些数据。最常见的使用场景是缓存相关的数据,这样一来,当设备无法访问网络时,用户仍然可以在离线状态下浏览该内容。
  • Room 持久性库在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。具体来说,Room 具有以下优势:
  • (1)针对 SQL 查询的编译时验证;
  • (2)可最大限度减少重复和容易出错的样板代码的方便注解;
  • (3)简化了数据库迁移路径。

2、Room 依赖

2.1 dependencies 配置
dependencies {
    val roomVersion = "2.4.2"

    implementation("androidx.room:room-runtime:$roomVersion")
    
    // 使用 Java 注解处理工具(annotationProcessor)
    annotationProcessor("androidx.room:room-compiler:$roomVersion")
    // 使用 Kotlin 注释处理工具 (kapt)
    kapt("androidx.room:room-compiler:$roomVersion")
    // 使用 Kotlin 符号处理 (ksp)
    ksp("androidx.room:room-compiler:$roomVersion")

    // 可选 - 对 Room 的 Kotlin 扩展和协程支持
    implementation("androidx.room:room-ktx:$roomVersion")

    // 可选 - RxJava2 对 Room 的支持
    implementation("androidx.room:room-rxjava2:$roomVersion")

    // 可选 - RxJava3 对 Room 的支持
    implementation("androidx.room:room-rxjava3:$roomVersion")

    // 可选 - 对 Room 的 Guava 支持,包括 Optional 和 ListenableFuture
    implementation("androidx.room:room-guava:$roomVersion")

    // 可选 - 测试助手
    testImplementation("androidx.room:room-testing:$roomVersion")

    // 可选 - Paging 3 集成
    implementation("androidx.room:room-paging:2.5.0-alpha01")
}
2.2 gradle 配置
  • Room 可以在编译时将数据库的架构信息导出为 JSON 文件。
android {
  ...
    defaultConfig {
      ...
        javaCompileOptions {
          annotationProcessorOptions {
            arguments = [
              "room.schemaLocation":"$projectDir/schemas".toString(),
              "room.incremental":"true",
              "room.expandProjection":"true"]
          }
        }
    }
}

  • room.schemaLocation: 输出数据库概要, 可以查看字段信息, 版本号, 数据库创建语句等
  • room.incremental: 启用 Gradle 增量注释处理器
  • room.expandProjection: 在使用星投影时会根据函数返回类型来重写 SQL 查询语句

3、Room 简单使用

3.1 Room 主要组件
  • Room 包含三个主要组件:
  • (1)数据实体,用于表示应用的数据库中的表;
  • (2)数据访问对象 (DAO),提供您的应用可用于插入、删除、更新和查询数据库中的数据的方法。
  • (3)数据库类,用于保存数据库并作为应用持久性数据底层连接的主要访问点;
    Room 库架构的示意图
3.2 数据实体
  • 定义数据实体,用于表示应用的数据库中的表(参考:Room 实体定义数据更详细使用文献)。
  • 举例:以下代码定义了一个 User 数据实体。User 的每个实例都代表应用数据库中 user 表中的一行。
/**
 * @Entity:用来修饰数据实体,其对应的就是一张表;
 * @Entity(tableName = "users"):默认情况下,Room 将类名称用作数据库表名称,也可以通过 tableName 属性自定义表名;
 * @Entity(primaryKeys = ["firstName", "lastName"]):定义表中复合主键;
 * @PrimaryKey:用来定义表中的单个主键;
 * @PrimaryKey(autoGenerate = true):用来定义表中的单个主键,并且自动分配,且自增;
 * @Ignore:忽略表中某个字段;
 * @Entity(ignoredColumns = ["picture"]):忽略表中多个字段;
 */ 
@Entity(tableName = "user")
data class UserEntity(
  @PrimaryKey var uid: Int,
  @ColumnInfo(name = "sid") var sid: Long,
  var cid: Long,
  var subIndex: Int,
  var name: String?,
  var age: Long,
  var bool: Boolean
)
3.3 数据访问对象 (DAO)
  • 定义数据访问对象 (DAO),提供您的应用可用于插入、删除、更新和查询数据库中的数据的方法(参考:Room DAO 访问数据更详细使用文献)。
  • 举例:以下代码定义了一个名为 UserDaoDAOUserDao 提供了应用的其余部分用于与 user 表中的数据交互的方法。
@Dao
interface UserDao {

  data class UserAttribute(val uid: Int, val sid: Long, val cid: Long)

  /** 插入表中某些数据 */
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  fun insert(vararg entitys: UserEntity): List<Long>

  /** 清除表中某些数据 */
  @Delete
  fun delete(vararg entitys: UserEntity): Int

  /** 清除表中所有数据 */
  @Query("DELETE FROM user")
  fun deleteAll(): Int

  /** 更新表中某些数据 */
  @Update
  fun update(vararg entitys: UserEntity): Int

  /** 查询表中所有数据 */
  @Query("SELECT * FROM user")
  fun queryAll(): List<UserEntity>

  /*** =================== 上述是一些通用性的增、删、改、查接口 =================== */

  /** 通过子集进行局部更新 */
  @Update(entity = UserEntity::class)
  fun updateUserAttribute(attribute: UserAttribute)

  /** 根据“[uid=?数组]”查询表中对应的数据 */
  @Query("SELECT * FROM user WHERE uid IN (:uids)")
  fun queryWithUids(uids: IntArray): List<UserEntity>

  /** 根据“[bool=?]”查询表中对应的数据 */
  @Query("SELECT * FROM user WHERE bool = :bool")
  fun queryWithBool(bool: Boolean): List<UserEntity>

  /** 根据“[bool=1]”查询表中对应的数据 */
  @Query("SELECT * FROM user WHERE bool = 1")
  fun queryWithBoolTure(bool: Boolean): List<UserEntity>

  /** 根据“[name=?]”并且"[offset=0&&limit=1]"查询表中对应的数据 */
  @Query("SELECT * FROM user WHERE name LIKE '%'||:user||'%' LIMIT 1")
  fun queryWithFirstNameAndLastName(user: String): UserEntity?

  /** 查询表中有多少条数据 */
  @Query("SELECT count(*) FROM user")
  fun queryAllCount(): Long

  /** 根据“[name=?&&cid=?]”查询表中"[classIndex]"字段对应最大的一条数据 */
  @Query("SELECT MAX(subIndex) FROM user WHERE sid = :sid AND cid = :cid")
  fun queryMaxClassIndex(sid: Long, cid: Long): Long

  /** 根据“[age=?]”查询表中年龄大于"[minAge]"对应的数据 */
  @Query("SELECT * FROM user WHERE age > :minAge")
  fun queryOlderThanAge(minAge: Int): Array<UserEntity>

  /** 根据“[age=?]”查询表中位于"[minAge]和[maxAge]之间对应的数据" */
  @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
  fun queryBetweenAge(minAge: Int, maxAge: Int): MutableList<UserEntity>
}
3.4 数据库类
  • 定义数据库配置,并作为应用对持久性数据的主要访问点。
  • 数据库类必须满足以下条件:
  • (1)该类必须带有 @Database 注解,该注解包含列出所有与数据库关联的数据实体的 entities 数组;
  • (2)该类必须是一个抽象类,用于扩展 RoomDatabase
  • (3)对于与数据库关联的每个 DAO 类,数据库类必须定义一个具有零参数的抽象方法,并返回 DAO 类的实例。
  • 注意:
  • (1)如果您的应用在单个进程中运行,在实例化 AppDatabase 对象时应遵循单例设计模式。每个 RoomDatabase 实例的成本相当高,而您几乎不需要在单个进程中访问多个实例;
  • (2)如果您的应用在多个进程中运行,请在数据库构建器调用中包含 enableMultiInstanceInvalidation()。这样,如果您在每个进程中都有一个 AppDatabase 实例,可以在一个进程中使共享数据库文件失效,并且这种失效会自动传播到其他进程中 AppDatabase 的实例。
@Database(
  entities = [UserEntity::class, ],
  // autoMigrations = [AutoMigration(from = x, to = x, spec = AppMigrationManage.AutoMigration_x_x::class)],
  version = 1,
  exportSchema = true
)
@TypeConverters(AppDatabaseConverter::class)
abstract class AppDatabase: RoomDatabase() {

  abstract fun userDao(): UserDao

  companion object {

    private val lock = Any()
    private val databases = mutableMapOf<Long, AppDatabase>()

    @Volatile
    private var MEMORY_INSTANCE: AppDatabase? = null
    private val factory = SupportFactory(SQLiteDatabase.getBytes(charArrayOf('1', '2', '3', '4', '5', '6')))

    @Synchronized
    internal fun getDatabase(loginId: Long = 0): AppDatabase =
      databases[loginId] ?: synchronized(lock) {
        databases[loginId] ?: buildDatabase(AppUtils.getAppContext(), loginId).also {databases[loginId] = it}
      }

    @JvmStatic
    fun getMemoryDatabase(context: Context): AppDatabase =
      MEMORY_INSTANCE ?: synchronized(this) {
        MEMORY_INSTANCE ?: buildDatabase(context).also {MEMORY_INSTANCE = it}
      }

    /**
     * 构建数据库
     * @param context 上下文
     * @param loginId 登录的Id
     */
    private fun buildDatabase(context: Context, loginId: Long = 0) =
      Room.databaseBuilder(
        context.applicationContext, AppDatabase::class.java, "database_${loginId}.db"
      ).openHelperFactory(factory)
        .addCallback(ClassInDatabaseCallback(context.applicationContext))
        // .addMigrations(AppMigrationManage.MIGRATION_x_x)
        .allowMainThreadQueries()
        .build()

    /**
     * 构建内存数据库
     * @param context 上下文
     */
    private fun buildMemoryDatabase(context: Context) =
      Room.inMemoryDatabaseBuilder(
        context.applicationContext, AppDatabase::class.java
      ).addCallback(ClassInDatabaseCallback(context.applicationContext))
        .build()

    private class ClassInDatabaseCallback(private val context: Context): RoomDatabase.Callback() {
      override fun onCreate(db: SupportSQLiteDatabase) {
        super.onCreate(db)
      }

      override fun onOpen(db: SupportSQLiteDatabase) {
        super.onOpen(db)
      }

      override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
        super.onDestructiveMigration(db)
      }
    }
  }

  @Synchronized
  fun cacheDatabase(block: (ArrayList<String>) -> Unit) {
    val array = arrayListOf<String>()
    val dbName = "database_${AccountPreference.getLoginId()}.db"
    val dbShmName = "database_${AccountPreference.getLoginId()}.db-shm"
    val dbWalName = "database_${AccountPreference.getLoginId()}.db-wal"
    if (AppUtils.getAppContext().getDatabasePath(dbName).exists()) array.add(dbName)
    if (AppUtils.getAppContext().getDatabasePath(dbShmName).exists()) array.add(dbShmName)
    if (AppUtils.getAppContext().getDatabasePath(dbWalName).exists()) array.add(dbWalName)
    block(array)
  }

  /**
   * 清空数据库
   */
  @Synchronized
  @Transaction
  fun clearDatabase() {
    appDatabase().let {
      if (it.isOpen) {
        try {
          it.userDao().deleteAll()
        } catch (e: Exception) {
          e.printStackTrace()
        }
      }
    }
  }
}

fun appDatabase(): AppDatabase = AppDatabase.getDatabase(AccountPreference.getLoginId())
3.5 调用
val userDao = appDatabase().userDao()
val users: List<UserEntity> = userDao.queryAll()

4、Room 的 7 个高级技巧

4.1 预填充数据库
  • 您是否需要在数据库创建后或打开数据库时将默认数据添加到数据库中?
  • 使用 RoomDatabase#addCallback() 在构建 RoomDatabase 时调用该方法并覆盖 onCreate()onOpen() 方法。
  • onCreate() 将在第一次创建数据库时调用,在创建表之后。onOpen() 在打开数据库时调用。由于只有在这些方法返回后才能访问 DAO,所以我们正在创建一个新线程,在其中我们获取对数据库的引用,获取 DAO,然后插入数据。
  • 注意:使用该 ioThread 方法时,如果您的应用程序在第一次启动时崩溃,在数据库创建和插入之间,数据将永远不会被插入。
Room.databaseBuilder(context. applicationContext , DataDatabase::class.java, "Sample.db") 
    // 在调用 onCreate 后预填充数据库
    .addCallback(object : Callback () { 
        override fun onCreate (db: SupportSQLiteDatabase) { 
            super. onCreate(db) 
            // 移动到一个新线程
            ioThread { 
                getInstance(context).dataDao() .insert(PREPOPULATE_DATA) 
            } 
        } 
    }) 
    .build()
4.2 使用 DAO 的继承能力
  • 您的数据库中是否有多个表并且发现自己在复制相同的 InsertUpdateDelete 方法?DAO 支持继承,因此创建一个类,并在那里 BaseDao<T> 定义您的泛型 @Insert 和方法。让每个 DAO 扩展并添加特定于它们的方法。
  • 注意:DAO 必须是接口或抽象类,因为 Room 在编译时生成它们的实现,包括来自 BaseDao
abstract class BaseDao<T> {

  /**
   * 获取表名
   */
  val tableName: String
    get() {
      return try {
        val clazz = (javaClass.superclass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<*>
        // 自定义一个将实体类映射到表名的方法
        AppDatabaseConstant.simpleNameConvertTabName(clazz.simpleName)
      } catch (e: Exception) {
        e.printStackTrace()
        ""
      }
    }

  @RawQuery
  abstract fun doRawQueryReturnInt(query: SupportSQLiteQuery): Int

  @RawQuery
  abstract fun doRawQueryReturnList(query: SupportSQLiteQuery): List<T>

  /** 插入表中某些数据 */
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  abstract fun insert(vararg entitys: T): List<Long>

  /** 清除表中某些数据 */
  @Delete
  abstract fun delete(vararg entitys: T): Int

  /** 清除表中所有数据 */
  fun deleteAll(): Int {
    val query = SimpleSQLiteQuery("DELETE FROM $tableName")
    return doRawQueryReturnInt(query)
  }

  /** 更新表中某些数据 */
  @Update
  abstract fun update(vararg entitys: T): Int

  /** 查询表中所有数据 */
  fun queryAll(): List<T> {
    val query = SimpleSQLiteQuery("SELECT * FROM $tableName")
    return doRawQueryReturnList(query) ?: emptyList()
  }
}

@Dao
abstract class UserDao : BaseDao<UserEntity>() {

    @Query("SELECT * FROM user WHERE uid IN (:uids)")
    fun queryWithUids(uids: IntArray): List<UserEntity>
}
4.3 用最少的样板代码在事务中执行查询
  • 注释方法 @Transaction 确保您在该方法中执行的所有数据库操作都将在一个事务中运行,当方法体中抛出异常时,事务将失败。
@Dao
abstract class UserDao {
    
    @Transaction
    open fun updateData(users: List<UserEntity>) {
        deleteAllUsers()
        insertAll(users)
    }
    
    @Insert
    abstract fun insertAll(users: List<UserEntity>)
    
    @Query("DELETE FROM Users")
    abstract fun deleteAllUsers()
}
4.4 只读你需要的
  • 查询数据库时,是否使用了查询中返回的所有字段?注意您的应用程序使用的内存量,并仅加载您最终将使用的字段子集。这也将通过降低 IO 成本来提高查询速度。Room 将为您完成列和对象之间的映射。
  • 考虑这个复杂的 User 对象:
@Entity(tableName = "user")
data class UserEntity(
  @PrimaryKey var uid: Int,
  @ColumnInfo(name = "sid") var sid: Long,
  var cid: Long,
  var subIndex: Int,
  var name: String,
  var age: Long,
  var bool: Boolean
)
  • 在某些屏幕上,我们不需要显示所有这些信息。因此,我们可以创建一个 UserMiniEntity 只保存所需数据的对象。
data class UserMiniEntity(
    var uid: Int,
    var name: String, 
    var age: Long
)
  • DAO 类中,我们定义查询并从 user 表中选择正确的列。
@Dao
interface UserDao {
    @Query("SELECT uid, name, age FROM user")
    fun queryUserMiniEntity(): List<UserMiniEntity>
}
4.5 在具有外键的实体之间实施约束
  • 尽管 Room 不直接支持关系,但它允许您定义实体之间的外键约束。
  • Room 具有 @ForeignKey 注释,这是 @Entity 注释的一部分,以允许使用 SQLite 外键功能。它在表之间强制实施约束,以确保在您修改数据库时关系有效。在实体上,定义要引用的父实体、其中的列以及当前实体中的列。
  • 考虑一个 UserEntity 和一个 PetEntity 类。 PetEntity 有一个 wid,它是作为外键引用的用户 uid
@Entity(
  tableName = "pet",
  foreignKeys = [ForeignKey(entity = UserEntity::class, parentColumns = arrayOf("uid"), childColumns = arrayOf("wid"))]
)
data class PetPetEntity(
  @PrimaryKey var petId: String,
  var wid: Long,
  var name: String
)
  • 或者,您可以定义在数据库中删除或更新父实体时要执行的操作。您可以选择以下之一:NO_ACTIONRESTRICTSET_NULLSET_DEFAULTCASCADE,它们的行为与 SQLite 中的相同。
  • 注意:在 Room 中,SET_DEFAULT 用作 SET_NULL,因为 Room 还不允许为列设置默认值。
4.6 通过简化一对多查询 @Relation
  • 4.5 的示例中,我们可以说我们有一个一对多的关系:一个用户可以拥有多个宠物。假设我们想要获取带有宠物的用户列表:List<UserAndAllPets>
data class UserAndAllPets (
    var user: UserEntity,
    var pets: List<PetPetEntity> = ArrayList()
)
  • 要手动执行此操作,我们需要实现 2 个查询:一个获取所有用户的列表,另一个获取基于用户 uid 的宠物列表。
@Query("SELECT * FROM user")
fun queryUsers(): List<UserEntity>

@Query("SELECT * FROM pet WHERE wid = :uid")
fun queryPetsForUser(uid: Int): List<PetPetEntity>
  • 然后我们将遍历用户列表并查询 pet 表。为了简化这一点,Room@Relation 注解会自动获取相关实体。 @Relation 只能应用于 ListSet 对象。 UserAndAllPets 类必须更新:
class UserAndAllPets {

  @Embedded
  var user: UserEntity? = null

  @Relation(parentColumn = "uid", entityColumn = "wid")
  var pets: List<PetPetEntity> = ArrayList()
}
  • DAO 中,我们定义了一个查询,Room 将同时查询 userpet 表并处理对象映射。
@Transaction
@Query("SELECT * FROM user")
fun queryUserAndAllPets(): List<UserAndAllPets>
4.7 避免可观察查询的误报通知
  • 假设您想根据可观察查询中的用户 uid 获取用户:
@Query("SELECT * FROM user WHERE uid = :uid")
fun getUserByUid(uid: Int): LiveData<UserEntity>

// 或者

@Query("SELECT * FROM user WHERE uid = :uid")
fun getUserByUid(uid: Int): Flowable<UserEntity>
  • UserEntity 每当该用户更新时,您都会获得该对象的新发射。UserEntity 但是当表上发生与您感兴趣的无关的其他更改(删除、更新或插入)时,您也会得到相同的对象 UserEntity,从而导致误报通知。更重要的是,如果您的查询涉及多个表,那么只要其中任何一个发生更改,您就会得到一个新的发射。
  • 以下是幕后发生的事情:
  • (1)SQLite 支持在表中发生 DELETEUPDATEINSERT 时触发的触发器。
  • (2)Room 创建了一个 InvalidationTracker,它使用 Observers 来跟踪观察到的表中何时发生了变化。
  • (3)LiveDataFlowable 查询都依赖于 InvalidationTracker.Observer#onInvalidated 通知,收到此信息后,它会触发重新查询。
  • Room 只知道该表已被修改,但不知道为什么以及发生了什么变化。因此,重新查询后,查询的结果由 LiveDataFlowable 发出。由于 Room 没有在内存中保存任何数据,并且不能假设对象具有 equals(),因此它无法判断这是否是相同的数据。
  • 你需要确保你的 DAO 过滤排放并且只对不同的对象做出反应。
  • 如果 observable 查询是使用 Flowables 实现的,请使用 Flowable#distinctUntilChanged
@Dao
abstract class UserDao : BaseDao<UserEntity>() {

    @Query("SELECT * FROM user WHERE uid = :uid")
    protected abstract fun queryByUid(uid: Int): Flowable<UserEntity>
    
    fun queryDistinctByUid(uid: Int): Flowable<UserEntity> = queryByUid(id).distinctUntilChanged()

}
  • 如果您的查询返回 LiveData,您可以使用仅允许来自源的不同对象发射的 MediatorLiveData
// 基于 LiveData 的扩展方法
fun <T> LiveData<T>.getDistinct(): LiveData<T> {
    val distinctLiveData = MediatorLiveData<T>()
    distinctLiveData.addSource(this, object: Observer<T> {
      private var initialized = false
      private var lastObj: T? = null
      override fun onChanged(obj: T?) {
        if (! initialized) {
          initialized = true
          lastObj = obj
          distinctLiveData.postValue(lastObj !!)
        } else if ((obj == null && lastObj != null) || obj != lastObj) {
          lastObj = obj
          distinctLiveData.postValue(lastObj !!)
        }
      }
    })
    return distinctLiveData
}
  • 在您的 DAO 中,将返回不同 LiveData 的方法设为 public 和查询受保护的数据库的方法。
@Dao
abstract class User2Dao: BaseDao<UserEntity>() {

  @Query("SELECT * FROM user WHERE uid = :uid")
  protected abstract fun queryByUid(uid: Int): LiveData<UserEntity>

  fun queryDistinctByUid(uid: Int): LiveData<UserEntity> = queryByUid(uid).getDistinct()
}

5、Room 数据库增量迁移

5.1 自动迁移
  • 注意:Room 自动迁移依赖于为旧版和新版数据库生成的数据库架构。如果 exportSchema 设为 false,或者如果您尚未使用新版本号编译数据库,自动迁移将会失败。
// 版本更新之前的数据库类
@Database(version = 1, entities = [User::class])
abstract class AppDatabase : RoomDatabase() {
  ...
}

// 版本更新后的数据库类
@Database(version = 2, entities = [User::class],
  autoMigrations = [
    AutoMigration (from = 1, to = 2)
  ]
)
abstract class AppDatabase : RoomDatabase() {
  ...
}
5.2 手动迁移
val MIGRATION_1_2 = object : Migration(1, 2) {
  override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " +
      "PRIMARY KEY(`id`))")
  }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
  override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
  }
}

Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
  .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()

6、Room 数据库文件备份拷贝

  • 建立好的数据库 db 文件都存在于 data/data/包名/databases 目录下,一般会对应有三个文件:
  • (1)xxx.db 文件:数据库文件
  • (2)xxx.db-shm 文件:
    • db-shm 文件是共享内存文件,仅当 SQLiteWAL(预写日志)模式运行时才存在。 这是因为在 WAL 模式下,共享同一个 db 文件的数据库连接必须全部更新同一存储位置(用作 WAL 文件的索引),以防止发生冲突。
  • (3)xxx.db-wal 文件:
    • wal 意思是 write-ahead log,顾名思义就是保存的一个日志,对于提交/回滚目的很有用。sqlite 3.7 之后开始提供这个功能,当一个数据库采用 WAL 模式,所有连接数据的操作都必须使用 WAL,然后在在数据库文件夹下生成一个后缀为 .db-wal 的文件保存操作日志。该日志使 SQLite 可以在事务失败时回滚更改。SQLite 如何使用它们以及为什么将它们保留这么长时间取决于 SQLite 的作者。如果数据库未在运行,则删除该文件是完全可以的,实际上,如果存在该文件,它将在重新启动数据库时自动删除(因为它仅在数据库正在主动写入/提交数据时才有用)
  • 参考资料:sqlite 官网:https://www.sqlite.org/fileformat2.html
  • 所以咱们对 Room 数据库文件的备份拷贝的时候,只需要对位于 data/data/包名/databases 目录下的三个文件进行备份拷贝就行了。
  • 数据库打开的工具可以使用:DB Browser for SQLite,选择 SQLCipher 4 defaults 选项打开,我使用对应的版本是 3.11.2,仅供参考。

7、Room 数据库文件加密

  • 调研了市场目前存在针对于 Room 数据库加密的方案后,SqlCipher 相对成熟并稳定。
  • 第一步:导包
dependencies {
  implementation "net.zetetic:android-database-sqlcipher:4.5.1"
  implementation "androidx.sqlite:sqlite:2.2.0"
}
  • 第二步:使用
abstract class AppDatabase: RoomDatabase() {
    private val factory = SupportFactory(SQLiteDatabase.getBytes(charArrayOf('1', '2', '3', '4')))
    
}

Room.databaseBuilder(
        context.applicationContext, AppDatabase::class.java, "database_${loginId}.db"
      ).openHelperFactory(factory).build()
  • 数据库打开的工具可以使用:DB Browser for SQLite,选择 SQLCipher 4 defaults 选项打开,我使用对应的版本是 3.11.2,仅供参考。

8、Room 数据库单元测试

  • 第一步:导包
android {

  compileSdkVersion 31

  defaultConfig {
    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  }
}

dependencies {
  
  // 测试
  androidTestImplementation 'androidx.test.ext:junit:1.1.3'
  testImplementation "androidx.room:room-testing:$room_version"
}
  • 第二步:对 Dao 接口写对应的单元测试
/**
 * UserDaoTest
 *
 * @Description: 用户 单元测试类
 * @see UserDao:用户 Dao 类
 * @see UserEntity:用户 Entity 类
 */
@RunWith(AndroidJUnit4::class)
class UserDaoTest {

  private lateinit var appDatabase: AppDatabase
  private lateinit var dao: UserDao

  @Before
  fun createDb() {
    val context = ApplicationProvider.getApplicationContext<Context>()
    appDatabase = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
    dao = appDatabase.userDao()
  }

  @After
  @Throws(IOException::class)
  fun closeDb() {
    appDatabase.close()
  }

  private fun createEntity(uid: Int) = UserEntity(
    uid = uid,
    sid = System.currentTimeMillis(), cid = System.currentTimeMillis(),
    subIndex = (0 .. 100).random(), name = "张三",
    age = (0 .. 100L).random(), bool = true
  )

  /** 插入表中某些数据 */
  @Test
  fun insert() {
    val entity = createEntity(1)
    val insert = dao.insert(entity)
    Assert.assertEquals(listOf(entity), dao.queryAll())
  }

  /** 清除表中某些数据 */
  @Test
  fun delete() {
    val entityArray = arrayOf(
      createEntity(1),
      createEntity(2)
    )
    dao.insert(*entityArray)
    val delete = dao.delete(entityArray[0])
    Assert.assertEquals(1, delete)
    Assert.assertEquals(listOf(entityArray[1]), dao.queryAll())
  }

  /** 清除表中所有数据 */
  @Test
  fun deleteAll() {
    val entityArray = arrayOf(
      createEntity(1),
      createEntity(2)
    )
    dao.insert(*entityArray)
    val delete = dao.deleteAll()
    Assert.assertEquals(2, delete)
  }

  /** 更新表中某些数据 */
  @Test
  fun update() {
    val entityArray = arrayOf(
      createEntity(1),
      createEntity(2)
    )
    dao.insert(*entityArray)
    entityArray[0].name = "testUpdate1"
    entityArray[1].name = "testUpdate2"
    val update = dao.update(*entityArray)
    Assert.assertEquals(2, update)
    Assert.assertEquals(entityArray.toList(), dao.queryAll())
  }

  /** 查询表中所有数据 */
  @Test
  fun queryAll() {
    val entityArray = arrayOf(
      createEntity(1),
      createEntity(2)
    )
    dao.insert(*entityArray)
    Assert.assertEquals(entityArray.toList(), dao.queryAll())
  }
}
  • 第三步:执行所有的测试用例,如果没有问题就说明接口的调用都是可以跑通的。

9、参考文献

  • Room 官方开发文档
  • https://www.sqlite.org/fileformat2.html
  • https://medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1
  • https://blog.csdn.net/qq_31469589/article/details/114950653
  • 等等…
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值