前言
SQLite 是 Android 系统内置的数据库。除了基本的增删改查外,我们还可以精准地控制事务 和平滑地实现数据库升级 ,来保障应用数据的健壮性 和一致性。
使用事务
SQLite 数据库支持事务,事务可以保证一系列操作要么全部成功,要么一个都不完成(全部回滚到未执行的状态),这被称为事务的原子性。
什么时候需要用到事务呢?
-
保障数据的一致性:当多个相关的操作需要同时成功或者同时失败时。例如银行转账,需要"转出账户扣钱"和"收款账号加钱"的两个操作绑定在一起,如果只有扣钱没有加钱,会导致数据的不一致性。
-
提高执行性能:当你需要批量执行插入、更新或删除操作时。默认情况下,每一次写入操作,数据库都会开启一个隐式事务并立即提交,这会导致频繁的磁盘 IO 操作。所以我们可以将多次操作,放在一个手动的事务中,这样可以提高性能。
我们来看看 Android 中如何使用事务。
例如,我们现在需要清空 Book 表的旧数据,并将新数据添加到表中。我们希望这两步操作要么都完成,要么一个都不完成,否则会导致数据不一致,比如表是空的,又或是新数据和旧数据同时存在。
首先,在布局中添加一个按钮,用于触发数据替换操作,代码如下:
xml
<Button
android:id="@+id/replaceData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Replace Data" />
在 MainActivity
中,实现按钮的点击逻辑,代码如下:
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
...
binding.replaceData.setOnClickListener {
// 启动一个协程,在 IO 线程池中执行数据库操作
lifecycleScope.launch(Dispatchers.IO) {
// 获取 SQLiteDatabase 对象
val db = dbHelper.writableDatabase
// 开启事务
db.beginTransaction()
try {
// 执行数据库操作
// 删除旧数据
db.delete("Book", null, null)
if (true) {
// 手动抛出一个异常,模拟事务中某个操作失败的情况
throw NullPointerException()
}
// 添加新数据(因为异常不会被执行到)
val values = ContentValues().apply {
put("name", "Game of Thrones")
put("author", "George Martin")
put("pages", 720)
put("price", 20.85)
}
db.insert("Book", null, values)
// 如果执行到这,说明事务执行成功
db.setTransactionSuccessful()
} catch (e: Exception) {
e.printStackTrace()
} finally {
// 结束事务
// 因为发生异常,setTransactionSuccessful() 方法没被调用,事务会自动回滚,数据库状态不变
db.endTransaction()
}
}
}
}
}
这就是事务的标准用法:
-
调用
SQLiteDatabase
的beginTransaction()
来开启一个事务,在try
代码块中执行所有的数据库操作。 -
所有操作完成后,调用其
setTransactionSuccessful()
方法表示事务执行成功,这样事务在结束时才会提交。 -
最后在
finally
代码块中,调用其endTransaction()
方法结束当前事务,不管是否发生异常,finnaly
语句块都会被会执行。
我们在其中手动抛出了异常,所以 setTransactionSuccessful()
方法不会被调用,即使执行了删除旧数据的代码,在结束事务时,会因事务失败,回滚所有操作。现在运行程序并点击按钮,发现旧数据还存在于数据表中。
transaction 扩展函数
当然我们可以通过 SQLiteDatabase
对象的 transaction()
扩展函数来自动管理事务状态,防止我们忘记调用事务的某个方法。例如:
kotlin
binding.replaceData.setOnClickListener {
// 启动一个协程,在 IO 线程池中执行耗时操作
lifecycleScope.launch(Dispatchers.IO) {
try {
// 自动管理事务状态:开始、成功、结束
dbHelper.writableDatabase.transaction {
// 删除旧数据
delete("Book", null, null)
// 手动抛出异常,模拟事务中的某个操作失败
if (true) {
throw IllegalStateException("A simulated error occurred!")
}
// 插入新数据(这段代码因为异常而不会被执行)
val values = ContentValues().apply {
put("name", "Game of Thrones")
put("author", "George Martin")
put("pages", 720)
put("price", 20.85)
}
insert("Book", null, values)
}
// 如果代码能执行到这里,说明事务已成功提交
Log.d("MainActivity","数据替换成功!")
} catch (e: Exception) {
// 事务会自动回滚,数据库状态不变
Log.e("DatabaseTransaction", "Transaction failed, rolling back.", e)
Log.d("MainActivity","操作失败,数据已回滚")
}
}
}
如果 transaction
的尾 Lambda 表达式没有抛出异常,说明所有操作都执行完毕了,事务会提交;否则,事务会回滚。
数据库升级
之前我们升级数据库的方式是非常简单粗暴的:直接在 onUpgrade()
方法中删除数据库中的所有数据表,然后调用 onCreate()
方法重新创建所有数据表,这样会导致用户的本地数据全部丢失,在真实项目中是决定不可以这样做的。
所以我们需要一个更合理的升级方案:可以为每一个版本号都对应一个数据库结构的变动,然后在 onUpgrade()
方法中根据当前版本号来执行对应的变动即可。
你可能听着有些云里雾里的,我们来通过一个具体的例子来演示。
假如数据库会有如下的变动:
- 版本1:有一张
Book
表。 - 版本2:新增一张
Category
表。 - 版本3:给
Book
表中新增一个category_id
字段。
MyDatabaseHelper
中的代码如下所示:
kotlin
class MyDatabaseHelper(private val context: Context, name: String, version: Int) :
SQLiteOpenHelper(context, name, null, version) {
// 版本一:创建 Book 表,也是版本三的最终表结构
private val createBook =
"""
create table Book (
id integer primary key autoincrement,
author text,
price real,
pages integer,
name text,
category_id integer
)
""".trimIndent()
// 版本二:创建 Category 表
private val createCategory =
"""
create table Category (
id integer primary key autoincrement,
category_name text,
category_code integer
)
""".trimIndent()
override fun onCreate(db: SQLiteDatabase) {
// 新安装的应用,直接创建最新数据库版本的表结构
db.execSQL(createBook)
db.execSQL(createCategory)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
// 处理版本升级
// oldVersion 是用户当前的数据库版本,newVersion 则是应用最新的版本
for (version in oldVersion until newVersion) {
when (version) {
1 -> {
// 从版本1升级到版本2: 创建 Category 表
db.execSQL(createCategory)
}
2 -> {
// 从版本2升级到版本3: 为 Book 表添加新字段
db.execSQL("alter table Book add column category_id integer")
}
}
}
}
}
这样为什么能够工作?
-
如果用户是直接安装最新版本的应用,会调用
onCreate()
方法,将最终版本的数据库结构创建好。 -
如果用户从版本1升级到版本3,则会调用
onUpgrade()
方法,将执行每次版本的数据库变更:先创建Category
表,然后为Book
表添加字段。这样能够无论用户是从哪一个旧版本,升级到最新版本,都能正确地更新数据库的结构,不会遗留任何变更。
Room 数据库
最后,SQLite 数据库是 Android 中原生的数据库技术,Google 目前推出了用于 Android 平台的 Room 数据库框架。
Room 是在 SQLite 之上提供的一个强大的抽象层。它能在编译期就验证 SQL 语句的正确性,让你不会因 SQL 拼写错误导致运行时发生异常。无需手动编写对于 Cursor
对象的解析操作,只需定义简单的 Dao
接口即可。Room 提升了开发效率和代码的健壮性。