【Android】数据库Room的利用总结

打印 上一主题 下一主题

主题 872|帖子 872|积分 2616

最近我负责开辟一个基于Android系统的平板应用程序,在项目中涉及到数据库操纵的部门,我们终极决定采用Room数据库框架来实现。在实际利用过程中,我遇到了一些挑衅和题目,现在我想将这些履历记载下来,以便未来参考和改进。
一、Room的基本利用

1.项目设置

在开辟这个Android项目时,我决定将数据库操纵代码独立成一个模块,这样做有助于保持代码的整洁和模块化。在这个模块中,我选择了Kotlin作为编程语言,并利用了Kotlin 1.5.21版本。为了支持Kotlin开辟和编译,我必要在项目中包罗两个插件:kotlin-android 和 kotlin-kapt。这两个插件分别负责Kotlin代码的Android特定功能支持和注解处理,确保代码能够正确编译和运行。
  1. plugins {
  2.     id 'com.android.library'
  3.     id 'kotlin-android'
  4.     id 'kotlin-kapt'
  5. }
复制代码
采用了Room框架,详细版本为2.3.0。由于Room框架在不同版本之间大概存在API差异,因此在这里特殊指出我所利用的版本,以便于在遇到题目时能够准确地查找和办理题目,同时也利用到了协程,所有依赖如下:
  1. dependencies {
  2.     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
  3.     // Room数据库版本
  4.     def room_version = "2.3.0"
  5.     implementation "androidx.room:room-runtime:$room_version"
  6.     // Kapt
  7.     kapt "androidx.room:room-compiler:$room_version"
  8.     // room-ktx
  9.     implementation "androidx.room:room-ktx:$room_version"
  10.     // 协程
  11.     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
  12.     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
  13. }
复制代码
2.创建实体类(Entity)

在Room数据库框架中,实体类是用来映射数据库表的。每个实体类代表一个数据库表,而实体类的属性则对应表中的列。以下是一个利用Kotlin语言编写的Airport实体类的示例代码,其中id字段被标记为主键:
  1. import androidx.room.Entity
  2. import androidx.room.PrimaryKey
  3. @Entity(tableName = "airports") // 指定表名
  4. data class Airport(
  5.     @PrimaryKey(autoGenerate = true) val id: Int, // 主键,自动生成
  6.     val name: String, // 机场名称
  7.     val city: String, // 所在城市
  8.     val country: String // 所在国家
  9. )
复制代码
3.创建数据访问对象(DAO - Data Access Object)

DAO 用于界说访问数据库的方法,好比插入、查询、更新、删除等操纵。以下是针对 Airport实体类创建的 AirportDao示例:
  1. import androidx.room.Dao
  2. import androidx.room.Insert
  3. import androidx.room.Query
  4. import androidx.room.Update
  5. import androidx.room.Delete
  6. @Dao
  7. interface AirportDao {
  8.     // 插入单个Airport对象
  9.     @Insert
  10.     suspend fun insert(airport: Airport)
  11.     // 插入多个Airport对象
  12.     @Insert
  13.     suspend fun insertAll(vararg airports: Airport)
  14.     // 根据ID查询Airport对象
  15.     @Query("SELECT * FROM airports WHERE id = :id")
  16.     suspend fun getAirportById(id: Int): Airport?
  17.     // 更新Airport对象
  18.     @Update
  19.     suspend fun update(airport: Airport)
  20.     // 删除单个Airport对象
  21.     @Delete
  22.     suspend fun delete(airport: Airport)
  23.     // 删除所有Airport对象
  24.     @Query("DELETE FROM airports")
  25.     suspend fun deleteAll()
  26. }
复制代码
在这个AirportDao接口中:


  • @Dao 注解标记这个接口为一个DAO接口。
  • @Insert注解用于界说插入操纵的方法。insert()方法可以插入单个Airport对象,而insertAll()方法可以插入多个Airport对象。
  • @Query 注解用于界说自界说SQL查询的方法。getAirportById()方法通过ID查询单个Airport对象。
  • @Update 注解用于界说更新操纵的方法。update()方法更新一个Airport对象。
  • @Delete注解用于界说删除操纵的方法。delete()方法可以删除单个Airport对象,而deleteAll()方法可以删除所有Airport对象。
  • suspend关键字用于标记一个函数为挂起函数(suspend function),这是Kotlin协程(coroutines)的一个紧张特性。挂起函数可以暂停和规复其实行,而不会壅闭线程
这些方法界说了与Airport实体类对应的数据库表举行交互的基本操纵。Room框架会在编译时主动实现这些接口方法,开辟者无需手动编写实现代码。
4. 创建数据库抽象类(Database)

在Room数据库框架中,你必要创建一个继承自RoomDatabase的抽象类,这个类将作为数据库的访问入口,并界说与实体类和DAO的关联。以下是一个示例代码,展示了怎样创建这样的数据库类,并与Airport实体类和AirportDao接口关联:
  1. import androidx.room.Database
  2. import androidx.room.RoomDatabase
  3. import androidx.room.TypeConverters
  4. // 定义数据库的版本
  5. @Database(entities = [Airport::class], version = 1, exportSchema = false)
  6. @TypeConverters(YourTypeConverters::class) // 如果有自定义类型转换器,在这里指定
  7. abstract class AppDatabase : RoomDatabase() {
  8.     // 提供获取DAO实例的方法
  9.     abstract fun airportDao(): AirportDao
  10.     // Companion object to create an instance of AppDatabase
  11.     companion object {
  12.         // Singleton instance of the database
  13.         @Volatile
  14.         private var instance: AppDatabase? = null
  15.         // Method to get the database instance
  16.         fun getInstance(context: Context): AppDatabase {
  17.             return instance ?: synchronized(this) {
  18.                 instance ?: buildDatabase(context).also { inst ->
  19.                     instance = inst
  20.                 }
  21.             }
  22.         }
  23.         // Method to build the database
  24.         private fun buildDatabase(context: Context): AppDatabase {
  25.             return Room.databaseBuilder(
  26.                 context.applicationContext,
  27.                 AppDatabase::class.java,
  28.                 "app_database"
  29.             ).build()
  30.         }
  31.     }
  32. }
复制代码
在这个AppDatabase类中:


  • @Database注解界说了数据库包罗的实体类(entities)和数据库版本(version)。exportSchema属性用于控制是否导出数据库的schema文件,通常在开辟阶段设置为true,而在生产环境中设置为false。
  • @TypeConverters注解用于指定一个或多个类,这些类包罗自界说的类型转换器,如果必要将非标准类型(如Date或URL)存储在数据库中,这些转换器是必须的。
  • abstract fun airportDao(): AirportDao提供了一个抽象方法,用于获取AirportDao的实例,这样我们就可以在数据库类中实行对Airport表的操纵。
  • companion
    object提供了一个单例模式的实现,用于获取AppDatabase的实例。getInstance()方法确保整个应用中只有一个数据库实例被创建。buildDatabase()方法用于实际构建和设置数据库。
**请注意!**你必要将YourTypeConverters::class替换为实际包罗自界说类型转换器的类的名称,如果你没有自界说类型转换器,可以省略@TypeConverters注解。别的,context参数必要从你的应用上下文传递给getInstance()方法,以确保数据库正确地与应用的生命周期关联。
5. 利用数据库

在Android的Activity中利用数据库举行操纵时,可以在协程中实行这些操纵,以制止壅闭主线程。以下是在Activity中利用协程与Room数据库举行交互的简朴示例代码片断:
  1. import android.os.Bundle
  2. import android.util.Log
  3. import androidx.activity.viewModels
  4. import androidx.appcompat.app.AppCompatActivity
  5. import androidx.lifecycle.ViewModelProvider
  6. import kotlinx.coroutines.*
  7. class AirportActivity : AppCompatActivity() {
  8.     private val viewModel by viewModels<AirportViewModel>()
  9.     override fun onCreate(savedInstanceState: Bundle?) {
  10.         super.onCreate(savedInstanceState)
  11.         setContentView(R.layout.activity_airport)
  12.         // 启动协程来插入数据
  13.         lifecycleScope.launch {
  14.             viewModel.insertAirport(Airport(0, "Moonshot International", "Shanghai", "China"))
  15.         }
  16.         // 启动协程来查询数据
  17.         lifecycleScope.launch {
  18.             val airport = viewModel.getAirportById(1) // 假设ID为1
  19.             airport.observe(this@AirportActivity, { airport ->
  20.                 Log.d("AirportActivity", "Airport Name: ${airport?.name}")
  21.             })
  22.         }
  23.     }
  24. }
复制代码
在这个Activity中:


  • lifecycleScope是一个与Activity生命周期绑定的协程作用域,它确保协程在Activity销毁时取消。
  • launch是一个协程构建器,用于启动一个新的协程。
  • insertAirport方法在协程中被调用,用于插入新的机场信息。
    getAirportById方法返回一个LiveData<Airport?>对象,它在协程中被观察,以便在机场信息变化时更新UI。
请注意,AirportViewModel必要正确实现,并且包罗insertAirport和getAirportById方法。这些方法应该在ViewModel中利用viewModelScope而不是lifecycleScope,因为viewModelScope是与ViewModel的生命周期绑定的,而不是Activity。
以下是AirportViewModel的示例实现:
  1. import androidx.lifecycle.ViewModel
  2. import androidx.lifecycle.viewModelScope
  3. import kotlinx.coroutines.*
  4. class AirportViewModel : ViewModel() {
  5.     private val database = AppDatabase.getInstance(applicationContext) // 假设这是全局可访问的context
  6.     private val airportDao = database.airportDao()
  7.     fun insertAirport(airport: Airport) {
  8.         viewModelScope.launch {
  9.             airportDao.insert(airport)
  10.         }
  11.     }
  12.     fun getAirportById(id: Int): LiveData<Airport?> {
  13.         return liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
  14.             emit(airportDao.getAirportById(id))
  15.         }
  16.     }
  17. }
复制代码
至此Room简朴的利用已经说完了,这些步骤构成了Room数据库在Android应用中的简朴利用流程。Room提供了一个抽象层,资助开辟者以更声明式和类型安全的方式举行数据库操纵,同时利用协程简化了异步编程。
二、Room利用过程遇到的题目

1.声明表中字段可以为null

  1. import androidx.room.Entity
  2. import androidx.room.PrimaryKey
  3. @Entity(tableName = "airports") // 指定表名
  4. data class Airport(
  5.     @PrimaryKey(autoGenerate = true) val id: Int, // 主键,自动生成
  6.     val name: String, // 机场名称
  7.     val city: String, // 所在城市
  8.     val country: String // 所在国家
  9. )
复制代码
如果在利用Room数据库时,必要在实体类中允许某些字段存储空值,可以直接将这些字段声明为可空类型。这样,即使在插入数据时这些字段的值为空,数据库操纵也能正常举行。详细来说,只需在实体类中将相应的变量声明为String?、Int?等可空类型,Room就会允许这些字段在数据库中存储空值,代码如下:
  1. import androidx.room.Entity
  2. import androidx.room.PrimaryKey
  3. @Entity(tableName = "airports") // 指定表名
  4. class Airport {
  5.     @PrimaryKey(autoGenerate = true)
  6.     var id: Int = 0 // 主键,自动生成
  7.     var name: String? = null // 机场名称
  8.     var city: String? = null// 所在城市
  9.     var country: String? = null // 所在国家
  10. }
复制代码
2.数据库升级

当你在Room数据库的实体类中添加了一个新的字段后,如果在运行应用时遇到了崩溃,并且出现了非常信息,这通常是因为Room数据库的迁移题目。Room必要知道怎样处理数据库布局的变化,好比添加、删除或修改字段。如果没有正确处理这些变化,Room在尝试访问数据库时就会抛出非常,非常信息如下:
   Room cannot verify the data integrity, Looks like vou’ve changed schema but forgot to update the version number, You can simply . fix this by increasing the version number.
  遇到数据库布局变更时,通常有两种处理方法:
第一种卸载并重新安装应用:这是一种简朴直接的方法,通过卸载应用再重新安装,应用将创建全新的数据库,从而主动包罗所有新的表布局和字段变更。另一种方法是举行数据库升级,下面是数据库升级的步骤:

  • 更新实体类:在实体类中添加新的字段。例如,如果你想为Airport实体类添加一个新字段,你可以直接声明这个字段为可空类型,如val newField: String? = null。
  • 增加数据库版本号:在@Database注解中增加版本号。例如,如果你的数据库当前版本是1,那么在添加新字段后,将版本号增加到2:@Database(entities = [Airport::class], version = 2)
  • 创建Migration迁移类:在RoomDatabase中界说Migration对象,指定怎样从旧版本迁移到新版本。例如,为Airport实体类添加新字段的迁移可以这样界说:
  1. val MIGRATION_1_2: Migration = object : Migration(1, 2) {
  2.    override fun migrate(database: SupportSQLiteDatabase) {
  3.         // 执行SQL
  4.        database.execSQL("ALTER TABLE airports ADD COLUMN newField TEXT")
  5.    }
  6. }
复制代码

  • 将Migration添加到数据库构建中:在构建数据库时,通过addMigrations方法添加Migration对象。例如
  1. val database = Room.databaseBuilder(
  2.     context.applicationContext,
  3.     AppDatabase::class.java,
  4.     "app_database"
  5. ).addMigrations(MIGRATION_1_2).build()
复制代码
这样,当应用启动时,Room会主动实行Migration中界说的迁移操纵。
通过这些步骤,你可以平滑地将Room数据库升级到新版本,同时添加新的字段。如果用户之前安装的数据库版本较低,Room会按照界说的Migration顺序依次实行,直到到达最新的数据库版本。
3.怎样关联外键ForeignKey

发现有些人不知道什么是外键:这里简朴说明一下:
外键的主要作用如下:

  • 建立关联关系:用于在不同表之间建立联系,清晰体实际体之间的对应关系,如机场与跑道的所属关系,方便举行多表联合查询等操纵。
  • 维护数据完整性:防止在插入或更新数据时出现无效数据,确保子表中的外键值在父表的主键值中存在或为 NULL,保证数据的准确性和一致性。
  • 实现级联操纵:界说级联规则,当父表中的记载发生删除或更新时,子表中对应的记载可按规则主动举行相应操纵,确保数据在不同表之间的和谐一致。
  • 束缚数据变化:通过参照完整性束缚,控制数据在表之间的更新和删除传播方式,保证数据的修改和更新符合特定的业务逻辑和要求。
在 Room 中声明外键可以通过在实体类中利用@ForeignKey注解来实现。以下是一个示例,展示了怎样在机场表和机场跑道表之间声明外键关联:

  • 界说机场表实体类
  1. import androidx.room.Entity
  2. import androidx.room.PrimaryKey
  3. @Entity(tableName = "airport")
  4. data class Airport(
  5.     @PrimaryKey(autoGenerate = true)
  6.     var airportId: Int = 0,
  7.     var airportName: String = ""
  8. )
复制代码

  • 界说机场跑道表实体类(Runway)并声明外键
  1. import androidx.room.Entity
  2. import androidx.room.ForeignKey
  3. import androidx.room.PrimaryKey
  4. @Entity(
  5.     tableName = "runway",
  6.     foreignKeys = [ForeignKey(
  7.         entity = Airport::class,
  8.         parentColumns = ["airportId"],
  9.         childColumns = ["airportIdFK"],
  10.         onDelete = ForeignKey.CASCADE
  11.     )]
  12. )
  13. data class Runway(
  14.     @PrimaryKey(autoGenerate = true)
  15.     var runwayId: Int = 0,
  16.     var airportIdFK: Int = 0,
  17.     var runwayName: String = ""
  18. )
复制代码
在上述 Runway 实体类中,利用 @ForeignKey 注解来声明外键关系,各参数寄义和 Java 版本中的一致:


  • entity:指定关联的实体类型,这里关联的是 Airport 类。
  • parentColumns:表示在关联的父实体(即 Airport)中对应的主键列名,此处为 airportId。
  • childColumns:代表在当前实体(Runway)中作为外键的列名,也就是 airportIdFK。
  • onDelete:界说了当父表(Airport 表)中对应的主键记载被删除时的行为,这里设置为 CASCADE,意味着级联删除,好比删除某个机场记载时,与之关联的跑道记载也会主动删除。除了 CASCADE 之外,另有以下几种常见的类型及其寄义:
   -NO_ACTION
寄义:当父表中的记载被删除或更新时,子表中的外键列不做任何操纵,这大概会导致子表中的外键引用无效的父键值,从而产生孤立的数据,破坏数据的完整性。
示例:在 文章表 和 评论表 的关联中,如果利用 NO_ACTION,当一篇文章被删除时,评论表中对应的文章外键值不会改变,仍然保存原来的文章 ID,即使该文章已经不存在了,这就导致了评论表中的这些评论与实际不存在的文章产生了孤立的关联。
SET_NULL
寄义:当父表中的记载被删除或更新时,子表中对应的外键列的值将被设置为 NULL。
示例:假设有 用户表 和 订单表,订单表 中的 用户ID 是外键关联到 用户表 的主键。当一个用户被删除时,该用户的所有订单记载中的 用户ID 字段将被设置为 NULL,表示这些订单与任何用户都不再关联,但订单记载本身仍然保存在 订单表 中。
SET_DEFAULT
寄义:当父表中的记载被删除或更新时,子表中对应的外键列的值将被设置为其默认值。
示例:若 订单表 中的 用户ID 外键字段有一个默认值为 0,当关联的用户被删除时,该用户的所有订单记载中的 用户ID 将被设置为 0,以此来表示一种特殊的状态或无关联的环境。
RESTRICT
寄义:当父表中的记载被删除或更新时,如果子表中存在对应的关联记载,则拒绝父表的删除或更新操纵,从而防止出现孤立的子记载,确保数据的一致性和完整性。
示例:在 部门表 和 员工表 的关系中,员工表 通过外键关联到 部门表 的主键。如果试图删除一个部门,而该部门下另有员工,那么由于 RESTRICT 束缚,数据库将不允许实行这个删除操纵,制止出现员工所属部门不存在的不合理环境。
  4.利用事务@Transaction

在 Room 中,事务是一种紧张的机制,用于确保多个数据库操纵的原子性,即要么所有操纵都乐成实行,数据库状态被完整更新;要么所有操纵都失败回滚,数据库保持初始状态,从而有用地维护数据的一致性。以下是关于 Room 中事务的详细介绍:
事务的须要性



  • 在实际的数据库操纵中,常常会有多个相关的操纵必要作为一个整体来实行。例如,在一个银行转账系统中,从一个账户扣除一定金额并在另一个账户增加相应金额,这两个操纵必须同时乐成或同时失败,否则就会导致数据不一致,如账户余额出现错误等题目。事务机制正是为了满足这种需求而计划的,它能够保证在复杂的操纵场景下数据的准确性和完整性。
利用方法

以下是一个利用事务举行多表查询的例子,还以Airport和Runway这两个实体类为例,它们之间存在关联关系。
  1. @Dao
  2. interface AirportRunwayDao {
  3.     @Query("SELECT * FROM airports WHERE id = :airportId")
  4.     fun getAirport(airportId: Int): Airport?
  5.     @Query("SELECT * FROM runways WHERE airportId = :airportId")
  6.     fun getRunways(airportId: Int): List<Runway>
  7.     // 事务性查询操作
  8.     @Transaction
  9.     fun getAirportWithRunways(airportId: Int): Pair<Airport?, List<Runway>> {
  10.         // 这里的代码将在一个事务中执行
  11.         val airport = getAirport(airportId)
  12.         val runways = getRunways(airportId)
  13.         return Pair(airport, runways)
  14.     }
  15. }
复制代码
5.数据库文件的位置

在Room数据库中,创建AppDatabase对象时,可以指定命据库文件的名称,这个名称也是数据库文件的名字。默认环境下,Room数据库文件存储在应用的内部存储目录下的特定子目录中。如果必要更改数据库文件的存储位置,可以通过指定详细的文件路径来实现。这样,数据库文件就会被创建在指定的路径下,而不是默认的内部存储位置。代码如下:
  1. private fun buildDatabase(context: Context): AppDatabase {
  2.             val dbPath = "${context.getExternalFilesDir(null)?.absolutePath}/database/test.db"
  3.             return Room.databaseBuilder(
  4.                 context.applicationContext,
  5.                 AppDatabase::class.java,
  6.                 dbPath
  7.             ).build()
  8.         }
复制代码
这样数据库文件存在的位置,就会放到指定目录下。
6.打开已存在的数据库

在大多数应用场景中,Room数据库的标准利用方法已经足够。但在本次项目中,我们必要软件具备打开本地已有数据库文件或导入外部数据库文件的功能。操纵步骤与常规设置相似:

  • 创建实体类(Entity):界说一个实体类来映射数据库中的表布局。
  • 创建数据访问对象(DAO):界说一个接口,用于实行数据库的增编削查等操纵。
与常规利用的主要区别在于,必要将待打开的数据库文件放置在指定目录下。在初始化Room数据库时,指定命据库文件的路径:


  • 如果指定路径下已存在数据库文件,Room将直接利用该文件。
  • 如果指定路径下没有数据库文件,Room将创建一个新的数据库文件。
    这样,我们就能够实现对本地或外部数据库文件的访问和管理。
打开外部数据时遇到的题目

当遇到
   IllegalStateException: Pre-packaged database has an invalid schema: airport
Expected…
…表布局信息
Found:
…表布局信息
  当遇到类似 “IllagelStateException: Pre-packaged database has an invalid schena: Excepted… Found:” 这样的报错时,其背后的缘故原由通常是预打包数据库(也就是你预备打开的外部数据文件对应的数据库)的架构与 Room 所盼望的架构出现了不匹配的环境。
那这里所说的数据库架构,涵盖了表布局、列界说以及束缚等多个方面的内容。常见的导致架构不匹配的因素有以下几种:

  • 一是字段可空声明不一致。好比在 Room中通过实体类界说某个字段优劣空的,但在预打包数据库里对应的该字段却允许为空,大概反之,这种差异就会造成架构不一致。
  • 二是数据类型不一致。大概在实体类中界说某个字段为 Integer 类型,然而预打包数据库里对应列的数据类型却是TEXT,不同的数据类型设置会让 Room 在验证数据库架构时判定为不匹配。
  • 三是外键束缚不一致。例如在 Room 的实体类中界说了两张表之间通过外键建立了特定的关联关系,并且设置了相应的外键束缚规则,像删除操纵时的级联方式等,但在预打包数据库里对应的表之间的外键束缚环境与之不同,这同样会引发架构方面的题目。
当出现这类报错后,我们必要细致对比非常日志里呈现的两个表布局,查找毕竟是哪个地方出现了不一致的环境。一旦发现了题目所在,接下来就要采取相应的办理措施。要么对 Room 中的 Entity 实体类举行修改,使其表布局、字段界说以及束缚等各方面与预打包数据库的实际架构相符;要么对预打包的数据库文件本身举行调整,从而让二者的布局能够达成一致。只有在确保这两个布局完全一致的条件下,才气够乐成连接数据库,制止出现上述的报错环境。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

水军大提督

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表