Android

关注公众号 jb51net

关闭
首页 > 软件编程 > Android > Android数据库Room使用

Android数据库Room的实际使用过程总结

作者:记忆里模糊的小时候

这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以参考下

前言

最近我负责开发一个基于Android系统的平板应用程序,在项目中涉及到数据库操作的部分,我们最终决定采用Room数据库框架来实现。在实际使用过程中,我遇到了一些挑战和问题,现在我想将这些经验记录下来,以便未来参考和改进。

一、Room的基本使用

1.项目配置

在开发这个Android项目时,我决定将数据库操作代码独立成一个模块,这样做有助于保持代码的整洁和模块化。在这个模块中,我选择了Kotlin作为编程语言,并使用了Kotlin 1.5.21版本。为了支持Kotlin开发和编译,我需要在项目中包含两个插件:kotlin-android 和 kotlin-kapt。这两个插件分别负责Kotlin代码的Android特定功能支持和注解处理,确保代码能够正确编译和运行。

plugins {
    id 'com.android.library'
    id 'kotlin-android'
    id 'kotlin-kapt'
}

采用了Room框架,具体版本为2.3.0。由于Room框架在不同版本之间可能存在API差异,因此在这里特别指出我所使用的版本,以便于在遇到问题时能够准确地查找和解决问题,同时也使用到了协程,所有依赖如下:

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    // Room数据库版本
    def room_version = "2.3.0"
    implementation "androidx.room:room-runtime:$room_version"
    // Kapt
    kapt "androidx.room:room-compiler:$room_version"
    // room-ktx
    implementation "androidx.room:room-ktx:$room_version"
    // 协程
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
}

2.创建实体类(Entity)

在Room数据库框架中,实体类是用来映射数据库表的。每个实体类代表一个数据库表,而实体类的属性则对应表中的列。以下是一个使用Kotlin语言编写的Airport实体类的示例代码,其中id字段被标记为主键:

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "airports") // 指定表名
data class Airport(
    @PrimaryKey(autoGenerate = true) val id: Int, // 主键,自动生成
    val name: String, // 机场名称
    val city: String, // 所在城市
    val country: String // 所在国家
)

3.创建数据访问对象(DAO - Data Access Object)

DAO 用于定义访问数据库的方法,比如插入、查询、更新、删除等操作。以下是针对 Airport实体类创建的 AirportDao示例:

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import androidx.room.Delete

@Dao
interface AirportDao {
    // 插入单个Airport对象
    @Insert
    suspend fun insert(airport: Airport)

    // 插入多个Airport对象
    @Insert
    suspend fun insertAll(vararg airports: Airport)

    // 根据ID查询Airport对象
    @Query("SELECT * FROM airports WHERE id = :id")
    suspend fun getAirportById(id: Int): Airport?

    // 更新Airport对象
    @Update
    suspend fun update(airport: Airport)

    // 删除单个Airport对象
    @Delete
    suspend fun delete(airport: Airport)

    // 删除所有Airport对象
    @Query("DELETE FROM airports")
    suspend fun deleteAll()
}

在这个AirportDao接口中:

这些方法定义了与Airport实体类对应的数据库表进行交互的基本操作。Room框架会在编译时自动实现这些接口方法,开发者无需手动编写实现代码。

4. 创建数据库抽象类(Database)

在Room数据库框架中,你需要创建一个继承自RoomDatabase的抽象类,这个类将作为数据库的访问入口,并定义与实体类和DAO的关联。以下是一个示例代码,展示了如何创建这样的数据库类,并与Airport实体类和AirportDao接口关联:

import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters

// 定义数据库的版本
@Database(entities = [Airport::class], version = 1, exportSchema = false)
@TypeConverters(YourTypeConverters::class) // 如果有自定义类型转换器,在这里指定
abstract class AppDatabase : RoomDatabase() {

    // 提供获取DAO实例的方法
    abstract fun airportDao(): AirportDao

    // Companion object to create an instance of AppDatabase
    companion object {
        // Singleton instance of the database
        @Volatile
        private var instance: AppDatabase? = null

        // Method to get the database instance
        fun getInstance(context: Context): AppDatabase {
            return instance ?: synchronized(this) {
                instance ?: buildDatabase(context).also { inst ->
                    instance = inst
                }
            }
        }

        // Method to build the database
        private fun buildDatabase(context: Context): AppDatabase {
            return Room.databaseBuilder(
                context.applicationContext,
                AppDatabase::class.java,
                "app_database"
            ).build()
        }
    }
}

在这个AppDatabase类中:

**请注意!**你需要将YourTypeConverters::class替换为实际包含自定义类型转换器的类的名称,如果你没有自定义类型转换器,可以省略@TypeConverters注解。此外,context参数需要从你的应用上下文传递给getInstance()方法,以确保数据库正确地与应用的生命周期关联。

5. 使用数据库

在Android的Activity中使用数据库进行操作时,可以在协程中执行这些操作,以避免阻塞主线程。以下是在Activity中使用协程与Room数据库进行交互的简单示例代码片段:

import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import kotlinx.coroutines.*

class AirportActivity : AppCompatActivity() {

    private val viewModel by viewModels<AirportViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_airport)

        // 启动协程来插入数据
        lifecycleScope.launch {
            viewModel.insertAirport(Airport(0, "Moonshot International", "Shanghai", "China"))
        }

        // 启动协程来查询数据
        lifecycleScope.launch {
            val airport = viewModel.getAirportById(1) // 假设ID为1
            airport.observe(this@AirportActivity, { airport ->
                Log.d("AirportActivity", "Airport Name: ${airport?.name}")
            })
        }
    }
}

在这个Activity中:

请注意,AirportViewModel需要正确实现,并且包含insertAirport和getAirportById方法。这些方法应该在ViewModel中使用viewModelScope而不是lifecycleScope,因为viewModelScope是与ViewModel的生命周期绑定的,而不是Activity。

以下是AirportViewModel的示例实现:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*

class AirportViewModel : ViewModel() {

    private val database = AppDatabase.getInstance(applicationContext) // 假设这是全局可访问的context
    private val airportDao = database.airportDao()

    fun insertAirport(airport: Airport) {
        viewModelScope.launch {
            airportDao.insert(airport)
        }
    }

    fun getAirportById(id: Int): LiveData<Airport?> {
        return liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(airportDao.getAirportById(id))
        }
    }
}

至此Room简单的使用已经说完了,这些步骤构成了Room数据库在Android应用中的简单使用流程。Room提供了一个抽象层,帮助开发者以更声明式和类型安全的方式进行数据库操作,同时利用协程简化了异步编程。

二、Room使用过程遇到的问题

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

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "airports") // 指定表名
data class Airport(
    @PrimaryKey(autoGenerate = true) val id: Int, // 主键,自动生成
    val name: String, // 机场名称
    val city: String, // 所在城市
    val country: String // 所在国家
)

如果在使用Room数据库时,需要在实体类中允许某些字段存储空值,可以直接将这些字段声明为可空类型。这样,即使在插入数据时这些字段的值为空,数据库操作也能正常进行。具体来说,只需在实体类中将相应的变量声明为String?、Int?等可空类型,Room就会允许这些字段在数据库中存储空值,代码如下:

import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "airports") // 指定表名
class Airport {
    @PrimaryKey(autoGenerate = true)
    var id: Int = 0 // 主键,自动生成
    var name: String? = null // 机场名称
    var city: String? = null// 所在城市
    var country: String? = null // 所在国家
}

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.

遇到数据库结构变更时,通常有两种处理方法:

第一种卸载并重新安装应用:这是一种简单直接的方法,通过卸载应用再重新安装,应用将创建全新的数据库,从而自动包含所有新的表结构和字段变更。另一种方法是进行数据库升级,下面是数据库升级的步骤:

val MIGRATION_1_2: Migration = object : Migration(1, 2) {
   override fun migrate(database: SupportSQLiteDatabase) {
        // 执行SQL
       database.execSQL("ALTER TABLE airports ADD COLUMN newField TEXT")
   }
}
val database = Room.databaseBuilder(
    context.applicationContext,
    AppDatabase::class.java, 
    "app_database"
).addMigrations(MIGRATION_1_2).build()

这样,当应用启动时,Room会自动执行Migration中定义的迁移操作。
通过这些步骤,你可以平滑地将Room数据库升级到新版本,同时添加新的字段。如果用户之前安装的数据库版本较低,Room会按照定义的Migration顺序依次执行,直到达到最新的数据库版本。

3.如何关联外键ForeignKey

发现有些人不知道什么是外键:这里简单说明一下:

外键的主要作用如下:

在 Room 中声明外键可以通过在实体类中使用@ForeignKey注解来实现。以下是一个示例,展示了如何在机场表和机场跑道表之间声明外键关联:

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "airport")
data class Airport(
    @PrimaryKey(autoGenerate = true)
    var airportId: Int = 0,
    var airportName: String = ""
)
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey

@Entity(
    tableName = "runway",
    foreignKeys = [ForeignKey(
        entity = Airport::class,
        parentColumns = ["airportId"],
        childColumns = ["airportIdFK"],
        onDelete = ForeignKey.CASCADE
    )]
)
data class Runway(
    @PrimaryKey(autoGenerate = true)
    var runwayId: Int = 0,
    var airportIdFK: Int = 0,
    var runwayName: String = ""
)

在上述 Runway 实体类中,使用 @ForeignKey 注解来声明外键关系,各参数含义和 Java 版本中的一致:

-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这两个实体类为例,它们之间存在关联关系。

@Dao
interface AirportRunwayDao {
    @Query("SELECT * FROM airports WHERE id = :airportId")
    fun getAirport(airportId: Int): Airport?

    @Query("SELECT * FROM runways WHERE airportId = :airportId")
    fun getRunways(airportId: Int): List<Runway>

    // 事务性查询操作
    @Transaction
    fun getAirportWithRunways(airportId: Int): Pair<Airport?, List<Runway>> {
        // 这里的代码将在一个事务中执行
        val airport = getAirport(airportId)
        val runways = getRunways(airportId)
        return Pair(airport, runways)
    }
}

5.数据库文件的位置

在Room数据库中,创建AppDatabase对象时,可以指定数据库文件的名称,这个名称也是数据库文件的名字。默认情况下,Room数据库文件存储在应用的内部存储目录下的特定子目录中。如果需要更改数据库文件的存储位置,可以通过指定具体的文件路径来实现。这样,数据库文件就会被创建在指定的路径下,而不是默认的内部存储位置。代码如下:

private fun buildDatabase(context: Context): AppDatabase {
            val dbPath = "${context.getExternalFilesDir(null)?.absolutePath}/database/test.db"
            return Room.databaseBuilder(
                context.applicationContext,
                AppDatabase::class.java,
                dbPath 
            ).build()
        }

这样数据库文件存在的位置,就会放到指定目录下。

6.打开已存在的数据库

在大多数应用场景中,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 中的 Entity 实体类进行修改,使其表结构、字段定义以及约束等各方面与预打包数据库的实际架构相符;要么对预打包的数据库文件本身进行调整,从而让二者的结构能够达成一致。只有在确保这两个结构完全一致的前提下,才能够成功连接数据库,避免出现上述的报错情况。

总结

到此这篇关于Android数据库Room的实际使用的文章就介绍到这了,更多相关Android数据库Room使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文