Android之数据持久化(sqllite数据库)四

Android之数据持久化(sqllite数据库)四

一,使用事务

前面我们已经知道,SQLite 数据库是支持事务的,事务的特性可以保证让某一系列的操
作要么全部完成,要么一个都不会完成。那么在什么情况下才需要使用事务呢?想象以下场
景,比如你正在进行一次转账操作,银行会将转账的金额先从你的账户中扣除,然后再向收
款方的账户中添加等量的金额。看上去好像没什么问题吧?可是,如果当你账户中的金额刚
刚被扣除,这时由于一些异常原因导致对方收款失败,这一部分钱就凭空消失了!当然银行
肯定已经充分考虑到了这种情况,它会保证扣钱和收款的操作要么一起成功,要么都不会成
功,而使用的技术当然就是事务了。
接下来我们看一看如何在 Android 中使用事务吧,仍然是在 DatabaseTest 项目的基础上
进行修改。比如 Book 表中的数据都已经很老了,现在准备全部废弃掉替换成新数据,可以
先使用delete()方法将Book表中的数据删除, 然后再使用insert()方法将新的数据添加到表中。
我们要保证的是,删除旧数据和添加新数据的操作必须一起完成,否则就还要继续保留原来
的旧数据。修改 activity_main.xml 中的代码,如下所示:


……

可以看到,这里又添加了一个按钮,用于进行数据替换操作。然后修改 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,这样添加新数据的代码就执行不到了。不过由于事务的存
在,中途出现异常会导致事务的失败,此时旧数据应该是删除不掉的。
现在可以运行一下程序并点击 Replacedata 按钮,你会发现,Book 表中存在的还是之前
的旧数据。然后将手动抛出异常的那行代码去除,再重新运行一下程序,此时点击一下
Replace data按钮就会将 Book 表中的数据替换成新数据了。

二,升级数据库的最佳写法

在前几节中我们学习的升级数据库的方式是非常粗暴的,为了保证数据库中的表是最
新的,我们只是简单地在 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 中的逻辑就会执行。
而如果用户是直接从第一版程序升级到第三版程序的,那么 case1 和 case2 中的逻辑都会执
行。使用这种方式来维护数据库的升级,不管版本怎样更新,都可以保证数据库的表结构是
最新的,而且表中的数据也完全不会丢失了。



你可能感兴趣的:(Android)