7.5.1 使用事务
SQLite 数据库是支持事务的,事务的特性可以保证让一些列的操作要么全部完成,要么一个都不会完成。那么在什么情况下才需要使用事务呢?想象以下场景,比如你正在执行一次转账操作,银行会先将转账的金额从你的账户中扣除,然后再向收款方的账户中添加等量的金额。看上去好像没什么问题吧?可是,如果当你账户中的金额刚刚被扣除,这是由于一些异常原因导致对方收款失败,这一部分钱就凭空消失了!当然银行肯定已经充分考虑到了这种情况,它会保证扣款和收款的操作要么一起成功,要么都不会成功。而使用的技术当然就是事务了。
接下来我们在Android 中使用事务,依然是在DatabaseTest 项目的基础上进行修改。比如Book 表中的数据已经很老了,现在准备全部废弃,替换成新数据,可以先使用delete() 方法将Book 表中的数据删除,然后再使用insert() 方法将新的数据添加到表中,我们需要保证删除旧数据和添加新数据的操作必须一起完成,否则就要继续保留原来的数据,我在布局文件中添加一个触发的按钮:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/createDatabase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Create Database"
/>
<Button
android:id="@+id/addData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add Data"
/>
<Button
android:id="@+id/updateData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Update Data"
/>
<Button
android:id="@+id/deleteData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Delete Data"
/>
<Button
android:id="@+id/queryData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Query Data"
/>
<Button
android:id="@+id/replaceData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Replace Data"
/>
</LinearLayout>
然后修改MainActivity 中的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
createDatabase.setOnClickListener {
// 当调用这个方法时,会检测当前程序中有没有这个数据库,如果没有就会调用onCreate 方法,如果有就不会调用onCreate。
dbHelper.writableDatabase
}
addData.setOnClickListener {
val db = dbHelper.writableDatabase // 获取数据库操作对象
val values1 = ContentValues().apply {
// 开始组装第一条数据
put("name","The Da Vinci Code")
put("author","Dan Brown")
put("pages",454)
put("price",16.96)
}
db.insert("Book",null,values1)
val values2 = ContentValues().apply {
// 开始组装第二条数据
put("name","The Lost Symbol")
put("author","Dan Brown")
put("pages",510)
put("price",19.35)
}
db.insert("Book",null,values2)
}
updateData.setOnClickListener {
val db = dbHelper.writableDatabase // 获取数据库操作对象
// 把要修改的数据封装好
val values = ContentValues().apply {
put("price",10.99)
}
// 这里第三个参数和第四个参数之间的关系还是需要有一些SQL 语言基础的。
db.update("Book",values,"name = ? AND id = ?", arrayOf("The Da Vinci Code","1"))
}
deleteData.setOnClickListener {
val db = dbHelper.writableDatabase // 获取数据库操作对象
db.delete("Book","pages > ?", arrayOf("500")) // 删除操作。
}
queryData.setOnClickListener {
val db = dbHelper.writableDatabase // 获取数据库操作对象
val cursor = db.query("Book", null, null, null, null, null, null)
// 如果有第一条数据 移动游标到第一条数据,如果有第一条数据移动成功则返回true
if (cursor.moveToFirst()){
// 遍历Cursor 对象,取出数据并打印
do {
// 遍历Cursor对象,取出数据并打印
val name = cursor.getString(cursor.getColumnIndex("name"))
val author = cursor.getString(cursor.getColumnIndex("author"))
val pages = cursor.getInt(cursor.getColumnIndex("pages"))
val price = cursor.getDouble(cursor.getColumnIndex("price"))
Log.d("MainActivity", "book name is $name")
Log.d("MainActivity", "book author is $author")
Log.d("MainActivity", "book pages is $pages")
Log.d("MainActivity", "book price is $price")
}while (cursor.moveToNext())
}
cursor.close() // 关闭Cursor
}
replaceData.setOnClickListener {
val db = dbHelper.writableDatabase // 获取数据库操作对象
db.beginTransaction() // 开启事务
db.delete("Book",null,null)
try {
if (true){
throw NullPointerException()
}
val values = ContentValues().apply {
put("name","Game of Thrones")
put("author","George Martin")
put("pages",728)
put("price",28.85)
}
db.insert("Book",null,values)
db.setTransactionSuccessful() // 事务已经执行成功 不设置会自动回滚不提交。
}catch (e:NullPointerException){
e.printStackTrace()
}finally {
db.endTransaction() // 结束事务
}
}
}
}
我们通过SQLiteDatabase 对象开启了一个事务,然后删除操作,接下来主动抛出个异常,在异常下进行了添加数据操作,已经对事务进行执行成功的调用,最后结束事务,在结束事务的时候由于我们抛出了异常,所以代码是走不到下面的 对事务进行执行成功的调用,所以事件会回滚,删除和添加都不会执行。
7.5.2 升级数据库的最佳写法
之前我们学习升级数据库的时候直接删除了所有的表,然后强制重新执行了一遍onCreate() 方法,这种方式在开发阶段是可以使用的,但是产品真正上线的时候就绝对不行了,这样的方式也非常的简单粗暴。
在我们升级应用覆盖安装的时候想要升级数据库总不能把用户的所有本地数据都清空了吧,其实只需要一些合理的控制,就可以保证升级数据库的时候数据不会丢失了。
每一个数据库版本都会对应一个版本号,指定的数据库版本号大于当前数据库版本号的时候,就会进入onUpgrade() 方法中执行更新操作。这里需要为每一个版本号赋予其所对应的数据库变动,然后在onUpgradle() 方法中对当前数据库的版本号进行判断,再执行相应的改变就可以了。
我们重新写一下MyDatabaseHelper 类的代码:
class MyDatabaseHelper(val context: Context,name:String,version: Int): SQLiteOpenHelper(context,name,null,version) {
private val createBook = "create table Book(" +
"id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text)"
private val createCategory = "create table Category(" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
}
}
只是在第一次执行的时候创建了一个Book表。
但是过了一段时间后有了新的需求,这次增加一个Category 表,于是乎代码改变了:
class MyDatabaseHelper(val context: Context,name:String,version: Int): SQLiteOpenHelper(context,name,null,version) {
private val createBook = "create table Book(" +
"id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text)"
private val createCategory = "create table Category(" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)
db.execSQL(createCategory) // 新用户直接创建表
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
// 第一个版本0-1 直接判断上一个版本是不是我们的第一个版本就行,因为Category 是第二个版本加进来的。
if (oldVersion <= 1){
db.execSQL(createCategory) // 老用户需要升级数据库
}
}
}
新用户下载程序的时候会直接创建这两个表,但是老用户我们就需要判断一下了,然后决定是否更新。
过一段又来新需求了,需求是给Book 表和 Category 表之间建立关联,需要在Book表中添加一个 category_id 字段,那么我们再次修改代码:
class MyDatabaseHelper(val context: Context,name:String,version: Int): SQLiteOpenHelper(context,name,null,version) {
private val createBook = "create table Book(" +
"id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text," +
"category_id integer)"
private val createCategory = "create table Category(" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)
db.execSQL(createCategory) // 新用户直接创建表
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
// 第一个版本0-1 直接判断上一个版本是不是我们的第一个版本就行,因为Category 是第二个版本加进来的。
if (oldVersion <= 1){
db.execSQL(createCategory) // 老用户需要升级数据库
}else if (oldVersion <= 2){
db.execSQL("alter table Book add column category_id integer") // 老用户为Book 表添加一个新的字段 category_id
}
}
}
这里 在我们第二个版本的时候进行了升级,我们在onUpgrade 方法中一定要做这样的if 判断,为每个版本做不同的升级逻辑。