前言
最近我负责开发一个基于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接口中:
- @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接口关联:
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类中:
- @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数据库进行交互的简单示例代码片段:
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中:
- lifecyclescope是一个与activity生命周期绑定的协程作用域,它确保协程在activity销毁时取消。
- launch是一个协程构建器,用于启动一个新的协程。
- insertairport方法在协程中被调用,用于插入新的机场信息。
getairportbyid方法返回一个livedata<airport?>对象,它在协程中被观察,以便在机场信息变化时更新ui。
请注意,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.
遇到数据库结构变更时,通常有两种处理方法:
第一种卸载并重新安装应用:这是一种简单直接的方法,通过卸载应用再重新安装,应用将创建全新的数据库,从而自动包含所有新的表结构和字段变更。另一种方法是进行数据库升级,下面是数据库升级的步骤:
- 更新实体类:在实体类中添加新的字段。例如,如果你想为airport实体类添加一个新字段,你可以直接声明这个字段为可空类型,如val newfield: string? = null。
- 增加数据库版本号:在@database注解中增加版本号。例如,如果你的数据库当前版本是1,那么在添加新字段后,将版本号增加到2:@database(entities = [airport::class], version = 2) 。
- 创建migration迁移类:在roomdatabase中定义migration对象,指定如何从旧版本迁移到新版本。例如,为airport实体类添加新字段的迁移可以这样定义:
val migration_1_2: migration = object : migration(1, 2) { override fun migrate(database: supportsqlitedatabase) { // 执行sql database.execsql("alter table airports add column newfield text") } }
- 将migration添加到数据库构建中:在构建数据库时,通过addmigrations方法添加migration对象。例如
val database = room.databasebuilder( context.applicationcontext, appdatabase::class.java, "app_database" ).addmigrations(migration_1_2).build()
这样,当应用启动时,room会自动执行migration中定义的迁移操作。
通过这些步骤,你可以平滑地将room数据库升级到新版本,同时添加新的字段。如果用户之前安装的数据库版本较低,room会按照定义的migration顺序依次执行,直到达到最新的数据库版本。
3.如何关联外键foreignkey
发现有些人不知道什么是外键:这里简单说明一下:
外键的主要作用如下:
建立关联关系:用于在不同表之间建立联系,清晰体现实体之间的对应关系,如机场与跑道的所属关系,方便进行多表联合查询等操作。
维护数据完整性:防止在插入或更新数据时出现无效数据,确保子表中的外键值在父表的主键值中存在或为 null,保证数据的准确性和一致性。
实现级联操作:定义级联规则,当父表中的记录发生删除或更新时,子表中对应的记录可按规则自动进行相应操作,确保数据在不同表之间的协调一致。
约束数据变化:通过参照完整性约束,控制数据在表之间的更新和删除传播方式,保证数据的修改和更新符合特定的业务逻辑和要求。
在 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 = "" )
- 定义机场跑道表实体类(runway)并声明外键
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 版本中的一致:
- 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这两个实体类为例,它们之间存在关联关系。
@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数据库的标准使用方法已经足够。但在本次项目中,我们需要软件具备打开本地已有数据库文件或导入外部数据库文件的功能。操作步骤与常规配置相似:
- 创建实体类(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 实体类进行修改,使其表结构、字段定义以及约束等各方面与预打包数据库的实际架构相符;要么对预打包的数据库文件本身进行调整,从而让二者的结构能够达成一致。只有在确保这两个结构完全一致的前提下,才能够成功连接数据库,避免出现上述的报错情况。
总结
到此这篇关于android数据库room的实际使用的文章就介绍到这了,更多相关android数据库room使用内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论