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

相关推荐
wu_android4 分钟前
android 网络视图 手机相册
android·智能手机
VirusVIP35 分钟前
解决:Android studio 编译后报错\app\src\main\cpp\CMakeLists.txt‘ to exist
android·ide·android studio
androidwork3 小时前
嵌套滚动交互处理总结
android·java·kotlin
fatiaozhang95274 小时前
中兴B860AV1.1强力降级固件包
android·adb·电视盒子·av1·机顶盒rom·魔百盒刷机
橙子199110165 小时前
Kotlin 中的 Object
android·开发语言·kotlin
AD钙奶-lalala9 小时前
android:foregroundServiceType详解
android
大胃粥12 小时前
Android V app 冷启动(13) 焦点窗口更新
android
fatiaozhang952716 小时前
中兴B860AV1.1_晨星MSO9280芯片_4G和8G闪存_TTL-BIN包刷机固件包
android·linux·adb·电视盒子·av1·魔百盒刷机
fatiaozhang952717 小时前
中兴B860AV1.1_MSO9280_降级后开ADB-免刷机破解教程(非刷机)
android·adb·电视盒子·av1·魔百盒刷机·移动魔百盒·魔百盒固件
二流小码农17 小时前
鸿蒙开发:绘制服务卡片
android·ios·harmonyos