SQLite 数据库的事务与无损升级

前言

SQLite 是 Android 系统内置的数据库。除了基本的增删改查外,我们还可以精准地控制事务 和平滑地实现数据库升级 ,来保障应用数据的健壮性一致性

使用事务

SQLite 数据库支持事务,事务可以保证一系列操作要么全部成功,要么一个都不完成(全部回滚到未执行的状态),这被称为事务的原子性

什么时候需要用到事务呢?

  1. 保障数据的一致性:当多个相关的操作需要同时成功或者同时失败时。例如银行转账,需要"转出账户扣钱"和"收款账号加钱"的两个操作绑定在一起,如果只有扣钱没有加钱,会导致数据的不一致性。

  2. 提高执行性能:当你需要批量执行插入、更新或删除操作时。默认情况下,每一次写入操作,数据库都会开启一个隐式事务并立即提交,这会导致频繁的磁盘 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()
                }
            }

        }
    }

}

这就是事务的标准用法:

  1. 调用 SQLiteDatabasebeginTransaction() 来开启一个事务,在 try 代码块中执行所有的数据库操作。

  2. 所有操作完成后,调用其 setTransactionSuccessful() 方法表示事务执行成功,这样事务在结束时才会提交。

  3. 最后在 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 提升了开发效率和代码的健壮性。

相关推荐
sam.li16 分钟前
WebView安全实现(一)
android·安全·webview
移动开发者1号1 小时前
Kotlin协程超时控制:深入理解withTimeout与withTimeoutOrNull
android·kotlin
程序员JerrySUN1 小时前
RK3588 Android SDK 实战全解析 —— 架构、原理与开发关键点
android·架构
移动开发者1号1 小时前
Java Phaser:分阶段任务控制的终极武器
android·kotlin
哲科软件10 小时前
跨平台开发的抉择:Flutter vs 原生安卓(Kotlin)的优劣对比与选型建议
android·flutter·kotlin
jyan_敬言16 小时前
【C++】string类(二)相关接口介绍及其使用
android·开发语言·c++·青少年编程·visual studio
程序员老刘17 小时前
Android 16开发者全解读
android·flutter·客户端
福柯柯18 小时前
Android ContentProvider的使用
android·contenprovider
不想迷路的小男孩18 小时前
Android Studio 中Palette跟Component Tree面板消失怎么恢复正常
android·ide·android studio
餐桌上的王子18 小时前
Android 构建可管理生命周期的应用(一)
android