6.5.1 使用事務android
前面咱們已經知道,SQLite 數據庫是支持事務的,事務的特性能夠保證讓某一系列的操 做要麼所有完成,要麼一個都不會完成。那麼在什麼狀況下才須要使用事務呢?想象如下場 景,好比你正在進行一次轉帳操做,銀行會將轉帳的金額先從你的帳戶中扣除,而後再向收 款方的帳戶中添加等量的金額。看上去好像沒什麼問題吧?但是,若是當你帳戶中的金額剛 剛被扣除,這時因爲一些異常緣由致使對方收款失敗,這一部分錢就憑空消失了!固然銀行 確定已經充分考慮到了這種狀況,它會保證扣錢和收款的操做要麼一塊兒成功,要麼都不會成 功,而使用的技術固然就是事務了。數據庫
接下來咱們看一看如何在 Android 中使用事務吧,仍然是在 DatabaseTest 項目的基礎上 進行修改。好比 Book 表中的數據都已經很老了,如今準備所有廢棄掉替換成新數據,能夠 先使用 delete()方法將 Book 表中的數據刪除,而後再使用 insert()方法將新的數據添加到表中。 咱們要保證的是,刪除舊數據和添加新數據的操做必須一塊兒完成,不然就還要繼續保留原來 的舊數據。修改 activity_main.xml 中的代碼,以下所示:ide
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"學習
android:orientation="vertical" >this
……spa
<Button android:id="@+id/replace_data" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Replace data"code
/>xml
</LinearLayout>事務
能夠看到,這裏又添加了一個按鈕,用於進行數據替換操做。而後修改 MainActivity 中 的代碼,以下所示:開發
public class MainActivity extends Activity {
private MyDatabaseHelper dbHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
……
Button replaceData = (Button) findViewById(R.id.replace_data);
replaceData.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransaction(); // 開啓事務
try {
db.delete("Book", null, null);
if (true) {
// 在這裏手動拋出一個異常,讓事務失敗
throw new NullPointerException();
}
ContentValues values = new ContentValues(); values.put("name", "Game of Thrones"); values.put("author", "George Martin"); values.put("pages", 720); values.put("price", 20.85); db.insert("Book", null, values);
db.setTransactionSuccessful(); // 事務已經執行成功
} catch (Exception e) {
e.printStackTrace();
} finally {
db.endTransaction(); // 結束事務
}
}
});
}
}
上述代碼就是 Android 中事務的標準用法,首先調用 SQLiteDatabase 的 beginTransaction() 方法來開啓一個事務,而後在一個異常捕獲的代碼塊中去執行具體的數據庫操做,當全部的 操做都完成以後,調用 setTransactionSuccessful()表示事務已經執行成功了,最後在 finally 代碼塊中調用 endTransaction()來結束事務。注意觀察,咱們在刪除舊數據的操做完成後手動 拋出了一個 NullPointerException,這樣添加新數據的代碼就執行不到了。不過因爲事務的存 在,中途出現異常會致使事務的失敗,此時舊數據應該是刪除不掉的。
如今能夠運行一下程序並點擊 Replace data 按鈕,你會發現,Book 表中存在的仍是以前的舊數據。而後將手動拋出異常的那行代碼去除,再從新運行一下程序,此時點擊一下Replace data 按鈕就會將 Book 表中的數據替換成新數據了。
6.5.2 升級數據庫的最佳寫法
在 6.4.2 節中咱們學習的升級數據庫的方式是很是粗暴的,爲了保證數據庫中的表是最 新的,咱們只是簡單地在 onUpgrade()方法中刪除掉了當前全部的表,而後強制從新執行了 一遍 onCreate()方法。這種方式在產品的開發階段確實能夠用,可是當產品真正上線了以後 就絕對不行了。想象如下場景,好比你編寫的某個應用已經成功上線,而且還擁有了不錯的 下載量。如今因爲添加新功能的緣由,使得數據庫也須要一塊兒升級,而後用戶更新了這個版 本以後發現之前程序中存儲的本地數據所有丟失了!那麼很遺憾,你的用戶羣體可能已經流 失一大半了。
聽起來好像挺恐怖的樣子,難道說在產品發佈出去以後還不能升級數據庫了?固然不 是,其實只須要進行一些合理的控制,就能夠保證在升級數據庫的時候數據並不會丟失了。 下面咱們就來學習一下如何實現這樣的功能,你已經知道,每個數據庫版本都會對應 一個版本號,當指定的數據庫版本號大於當前數據庫版本號的時候,就會進入到 onUpgrade() 方法中去執行更新操做。這裏須要爲每個版本號賦予它各自改變的內容,而後在onUpgrade()方法中對當前數據庫的版本號進行判斷,再執行相應的改變就能夠了。 接着就讓咱們來模擬一個數據庫升級的案例,仍是由 MyDatabaseHelper 類來對數據庫進行管理。初版的程序要求很是簡單,只須要建立一張 Book 表,MyDatabaseHelper 中的 代碼以下所示:
public class MyDatabaseHelper extends SQLiteOpenHelper {
public static final String CREATE_BOOK = "create table Book ("
+ "id integer primary key autoincrement, "
+ "author text, "
+ "price real, "
+ "pages integer, "
+ "name text)";
public MyDatabaseHelper(Context context, String name, CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
不過,幾星期以後又有了新需求,此次須要向數據庫中再添加一張 Category 表。因而, 修改 MyDatabaseHelper 中的代碼,以下所示:
public class MyDatabaseHelper extends SQLiteOpenHelper {
public static final String CREATE_BOOK = "create table Book ("
+ "id integer primary key autoincrement, "
+ "author text, "
+ "price real, "
+ "pages integer, "
+ "name text)";
public static final String CREATE_CATEGORY = "create table Category ("
+ "id integer primary key autoincrement, "
+ "category_name text, "
+ "category_code integer)";
public MyDatabaseHelper(Context context, String name, CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK);
db.execSQL(CREATE_CATEGORY);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
switch (oldVersion) {
case 1:
db.execSQL(CREATE_CATEGORY);
default:
}
}
}
能夠看到,在 onCreate()方法裏咱們新增了一條建表語句,而後又在 onUpgrade()方法中 添加了一個 switch 判斷,若是用戶當前數據庫的版本號是 1,就只會建立一張 Category 表。 這樣當用戶是直接安裝的第二版的程序時,就會將兩張表一塊兒建立。而當用戶是使用第二版 的程序覆蓋安裝初版的程序時,就會進入到升級數據庫的操做中,此時因爲 Book 表已經 存在了,所以只須要建立一張 Category 表便可。
可是沒過多久,新的需求又來了,此次要給 Book 表和 Category 表之間創建關聯,須要 在 Book 表中添加一個 category_id 的字段。再次修改 MyDatabaseHelper 中的代碼,以下所示:
public class MyDatabaseHelper extends SQLiteOpenHelper {
public static final String CREATE_BOOK = "create table Book ("
+ "id integer primary key autoincrement, "
+ "author text, "
+ "price real, "
+ "pages integer, "
+ "name text, "
+ "category_id integer)";
public static final String CREATE_CATEGORY = "create table Category ("
+ "id integer primary key autoincrement, "
+ "category_name text, "
+ "category_code integer)";
public MyDatabaseHelper(Context context, String name, CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_BOOK); db.execSQL(CREATE_CATEGORY);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
switch (oldVersion) {
case 1:
db.execSQL(CREATE_CATEGORY);
case 2:
db.execSQL("alter table Book add column category_id integer");
default:
}
}
}
能夠看到,首先咱們在 Book 表的建表語句中添加了一個 category_id 列,這樣當用戶直 接安裝第三版的程序時,這個新增的列就已經自動添加成功了。然而,若是用戶以前已經安 裝了某一版本的程序,如今須要覆蓋安裝,就會進入到升級數據庫的操做中。在 onUpgrade() 方法裏,咱們添加了一個新的 case,若是當前數據庫的版本號是 2,就會執行 alter 命令來爲 Book 表新增一個 category_id 列。
這裏請注意一個很是重要的細節,switch 中每個 case 的最後都是沒有使用 break 的, 爲何要這麼作呢?這是爲了保證在跨版本升級的時候,每一次的數據庫修改都能被所有執 行到。好比用戶當前是從第二版程序升級到第三版程序的,那麼 case 2 中的邏輯就會執行。 而若是用戶是直接從初版程序升級到第三版程序的,那麼 case 1 和 case 2 中的邏輯都會執 行。使用這種方式來維護數據庫的升級,無論版本怎樣更新,均可以保證數據庫的表結構是 最新的,並且表中的數據也徹底不會丟失了。