本章主要講了如何使用 SQLite 進(jìn)行持久化存儲(chǔ),包含了 CRUD 四個(gè)操作,使用基礎(chǔ)的 SQLiteOpenHelper 與 Cursor 構(gòu)造程序
GitHub 地址:
完成14章所有內(nèi)容
為什么要用數(shù)據(jù)庫(kù)而不是文本文件來(lái)存儲(chǔ)數(shù)據(jù)呢?
因?yàn)槿绻闷胀ǖ奈谋疚募ū热?txt)存儲(chǔ)數(shù)據(jù),每次讀取時(shí)都要讀取整個(gè)文件的內(nèi)容,完成修改后再全部保存,一旦數(shù)據(jù)量較大,將十分耗費(fèi)時(shí)間和資源。
SQLite是類似于MySQL的開(kāi)源關(guān)系型數(shù)據(jù)庫(kù)。不同于其他數(shù)據(jù)庫(kù)的是,SQLite使用單個(gè)文件存儲(chǔ)數(shù)據(jù),使用SQLite庫(kù)讀取數(shù)據(jù)。Android標(biāo)準(zhǔn)庫(kù)包含SQLite庫(kù)以及配套的一些Java輔助類。
1. 定義 Schema(架構(gòu))
本應(yīng)用需要保存的應(yīng)該是一個(gè) Crime 的全部數(shù)據(jù),比如一個(gè)表格可以下表這樣。
| _id | uuid | title | date | solved |
|---|---|---|---|---|
| 1 | 13090624138324 | Stolen yougurt | 13090636733242 | 0 |
| 2 | 13090274859392 | Dirty sink | 13090732131909 | 1 |
程序員的一個(gè)目標(biāo),或者說(shuō)信條,就是“不要重復(fù)造輪子”,也就是說(shuō),多花時(shí)間思考復(fù)用代碼的編寫和調(diào)用,避免在應(yīng)用中到處使用重復(fù)代碼。
基于上述準(zhǔn)則,我們可以使用能統(tǒng)一定義模型層對(duì)象的高級(jí) ORM(對(duì)象關(guān)系映射)工具,不過(guò)對(duì)于本章代碼來(lái)說(shuō),因?yàn)樾枰莆崭踊A(chǔ)的內(nèi)容,將會(huì)自己實(shí)現(xiàn)數(shù)據(jù)庫(kù)操作。
首先創(chuàng)建一個(gè) Package,名為 database, 然后定義數(shù)據(jù)架構(gòu)類 CrimeDbSchema.java
// CrimeDbSchema.java
public class CrimeDbSchema {
public static final class CrimeTable {
public static final String NAME = "crimes";
public static final class Cols {
public static final String UUID = "uuid";
public static final String TITLE = "title";
public static final String DATE = "date";
public static final String SOLVED = "solved";
}
}
}
這里有一個(gè)小 Tip,Android Studio 中有輸入一長(zhǎng)串前綴的 Live Template,比如要輸入
public static final String,只需要打 psfs 即可。
2. 初始創(chuàng)建數(shù)據(jù)庫(kù)
一般來(lái)說(shuō),在實(shí)際開(kāi)發(fā)中,打開(kāi)一個(gè)數(shù)據(jù)庫(kù)之前,由于不知道其是否存在,是否有更新,所以要經(jīng)過(guò)如下步驟:
- 確認(rèn)目標(biāo)數(shù)據(jù)庫(kù)是否存在。
- 如果不存在,首先創(chuàng)建數(shù)據(jù)庫(kù),然后創(chuàng)建數(shù)據(jù)庫(kù)表以及必需的初始化數(shù)據(jù)。
- 如果存在,打開(kāi)并確認(rèn)數(shù)據(jù)庫(kù)架構(gòu)是否為最新版本
- 如果是舊版,就運(yùn)行相關(guān)代碼升級(jí)到最新版本
在 Android 中,提供了一個(gè)SQLiteOpenHelper類用于處理這些打開(kāi)數(shù)據(jù)庫(kù)時(shí)繁雜的工作。我們可以創(chuàng)建一個(gè)SQLiteOpenHelper的子類用于對(duì)自己的數(shù)據(jù)庫(kù)進(jìn)行處理,比如:
public class CrimeBaseHelper extends SQLiteOpenHelper {
public static final int VERSION = 1;
public static final String DATABASE_NAME = "crimeBase.db";
public CrimeBaseHelper(Context context) {
super(context, DATABASE_NAME, null, VERSION);
}
// 如果數(shù)據(jù)庫(kù)不存在,就調(diào)用該函數(shù)創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)
@Override
public void onCreate(SQLiteDatabase db) {
// 一定要注意語(yǔ)句之間的空格,因?yàn)檎Z(yǔ)句是一個(gè)字符串
// 如果沒(méi)有空格,對(duì)于 SQL 來(lái)說(shuō),就是無(wú)意義的語(yǔ)句
// 比如下面的 table 后面一定要接一個(gè)空格
db.execSQL("create table " + CrimeTable.NAME + "(" +
" _id integer primary key autoincrement, " +
CrimeTable.Cols.UUID + ", " +
CrimeTable.Cols.TITLE + ", " +
CrimeTable.Cols.DATE + ", " +
CrimeTable.Cols.SOLVED +
")"
);
}
// 如果版本升級(jí)了,就調(diào)用 onUpgrade() 函數(shù)
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
由于數(shù)據(jù)庫(kù)結(jié)構(gòu)調(diào)整太麻煩,在實(shí)際開(kāi)發(fā)中最好的做法應(yīng)該是直接刪除數(shù)據(jù)庫(kù)文件(最方便的就是卸載程序) :)
3. 打開(kāi)數(shù)據(jù)庫(kù)
然后在 Model 層(在這里是 CrimeLab 單例)中打開(kāi)數(shù)據(jù)庫(kù)
mContext = context.getApplicationContext();
mDataBase = new CrimeBaseHelper(mContext)
.getWritableDatabase();
4. 寫入數(shù)據(jù)庫(kù)(插入與更新)
4.1 使用 ContentValues
負(fù)責(zé)處理數(shù)據(jù)庫(kù)寫入和更新操作的輔助類是 ContentValues 類。它是個(gè)鍵值存儲(chǔ)類,類似于 Java 的 HashMap(查看源碼可以發(fā)現(xiàn)它就是一個(gè) HashMap)和前面用過(guò)的 Bundle。不同的是, ContentValues 專門用于處理SQLite數(shù)據(jù)。我們需要一個(gè) Model 層的方法,把 Model 層數(shù)據(jù)轉(zhuǎn)換為ContentValues,由于在 Model 層外,對(duì)數(shù)據(jù)的操作應(yīng)該只有對(duì) Model 對(duì)象的操作,所以這個(gè)方法應(yīng)該是私有的。示例如下:
private static ContentValues getContentValues(Crime crime) {
ContentValues values = new ContentValues();
values.put(Cols.UUID, crime.getId().toString());
values.put(Cols.TITLE, crime.getTitle());
values.put(Cols.DATE, crime.getDate().getTime());
values.put(Cols.SOLVED, crime.isSolved() ? 1 : 0);
return values;
}
4.2 插入和更新記錄
4.2.1 插入記錄
準(zhǔn)備好了 ContentValues,就可以進(jìn)行寫入數(shù)據(jù)了,調(diào)用SQLiteDatabase.insert(…)方法即可。
public void addCrime(Crime c) {
ContentValues values = getContentValues(c);
/**
* SQLiteDatabase.insert(String table, String nullColumnHack, ContentValues values)
* 第一個(gè)參數(shù)是表名,第三個(gè)參數(shù)是鍵值對(duì)
* 第二個(gè)參數(shù)則是當(dāng) values 為全空時(shí)插入空行
* 如果設(shè)為 null,則 values 為全空時(shí)不插入空行
*/
mDatabase.insert(CrimeTable.NAME, null, values);
}
4.2.2 更新記錄
更新記錄使用的是SQLiteDatabase.update(…)方法。
public void updateCrime(Crime crime) {
String uuidString = crime.getId().toString();
ContentValues values = getContentValues(crime);
mDatabase.update(CrimeTable.NAME, values,
Cols.UUID + "=?",
new String[] {uuidString});
}
update 方法的原型是:
SQLiteDatabase.update(String table, // 表名
ContentValues values, // 鍵值對(duì)
// where 后面接的語(yǔ)句,一般是 "columnName = ?"
String whereClause,
// whereClause 中 ? 代表的語(yǔ)句,可以有多個(gè)
String[] whereArgs
);
可以看到,實(shí)際上后面兩個(gè)參數(shù)的意思就是 where columnName = columnValue,那么為什么要留出兩個(gè)參數(shù)而不用一個(gè)參數(shù)解決呢?
這樣做是為了防范 SQL 腳本注入,因?yàn)?String 如果本身就帶了 SQL 語(yǔ)句,如果不加處理放進(jìn)數(shù)據(jù)庫(kù)執(zhí)行,就有可能造成災(zāi)難性的后果(比如直接 drop 掉所有的表)
5. 讀取數(shù)據(jù)庫(kù)
讀取數(shù)據(jù)庫(kù)用到的是 query() 方法,這個(gè)方法有許多個(gè)重載版本,我們使用下面的版本:
public Cursor query(
String table, // 表名
String[] columns, // 選中的列名,為 null 時(shí)選中所有列
String where, // where 語(yǔ)句
String[] whereArgs, // where 語(yǔ)句的參數(shù)
String groupBy, // 分組
String having, // 與合計(jì)函數(shù)一起使用的 having
String orderBy, // 順序
String limit) // 限制數(shù)量
可以看到返回的是一個(gè) Cursor 對(duì)象,下面來(lái)探究一下 Cursor 對(duì)象
5.1 Cursor 與 CursorWrapper
Cursor 是個(gè)神奇的表數(shù)據(jù)處理工具,其任務(wù)就是封裝數(shù)據(jù)表中的原始字段值。從Cursor獲取數(shù)據(jù)的代碼大致如下所示:
String uuidString = cursor.getString(cursor.getColumnIndex(CrimeTable.Cols.UUID));
每次要取出一條記錄中的一列,都要重復(fù)寫一次上述代碼,所以我們使用 CursorWrapper 建立一個(gè) Cursor 的子類,在其中封裝可以轉(zhuǎn)換對(duì)象的方法。
比如一個(gè)類可以這么寫:
public class CrimeCursorWrapper extends CursorWrapper {
public CrimeCursorWrapper(Cursor cursor) {
super(cursor);
}
public Crime getCrime() {
//這里是從得到的 CursorWrapper 中取出數(shù)據(jù)
String uuidString = getString(getColumnIndex(Cols.UUID));
String title = getString(getColumnIndex(Cols.TITLE));
long date = getLong(getColumnIndex(Cols.DATE));
int isSolved = getInt(getColumnIndex(Cols.SOLVED));
// 然后生成一個(gè) Model 層對(duì)象返回,免去了重復(fù)寫的繁瑣
Crime crime = new Crime(UUID.fromString(uuidString));
crime.setTitle(title);
crime.setDate(new Date(date));
crime.setSolved(isSolved != 0);
return crime;
}
}
5.2 創(chuàng)建 Model 層對(duì)象
首先封裝一個(gè)數(shù)據(jù)庫(kù)查詢方法,返回的是自定義的 CrimeCursorWrapper 對(duì)象。
private CrimeCursorWrapper queryCrimes(String whereClause, String[] whereArgs) {
Cursor cursor = mDatabase.query(
CrimeTable.NAME,
null, // Columns -- use null to select all columns
whereClause,
whereArgs,
null,
null,
null
);
return new CrimeCursorWrapper(cursor);
}
再?gòu)?CursorWrapper 中獲取數(shù)據(jù)
// 獲取所有數(shù)據(jù)
public List<Crime> getCrimes() {
List<Crime> crimes = new ArrayList<>();
CrimeCursorWrapper cursor = queryCrimes(null, null);
try {
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
crimes.add(cursor.getCrime());
cursor.moveToNext();
}
} finally {
cursor.close();
}
return crimes;
}
//獲取單個(gè)記錄
public Crime getCrime(UUID id) {
CrimeCursorWrapper cursor = queryCrimes(
Cols.UUID + " = ?",
new String[] { id.toString() }
);
try {
if (cursor.getCount() == 0) {
return null;
}
cursor.moveToFirst();
return cursor.getCrime();
} finally {
cursor.close();
}
}
6 Application Context(應(yīng)用上下文)
前面,我們?cè)贑rimeLab的構(gòu)造方法中使用了Application Context。
private CrimeLab(Context context) {
mContext = context.getApplicationContext();
...
}
Application Context 有什么特別呢?就上例來(lái)看,為什么要用 Application Context ,而不直接用activity 作為 context 呢?
要回答上述問(wèn)題,關(guān)鍵就在于考慮它們的生命周期。只要有 activity 在,Android 肯定也創(chuàng)建 有 application 對(duì)象。用戶在應(yīng)用的不同界面間導(dǎo)航時(shí),各個(gè) activity 時(shí)而存在時(shí)而消亡,但 application 對(duì)象不會(huì)受任何影響??梢哉f(shuō),它的生命周期要比任何 activity 都要長(zhǎng)。
CrimeLab 是個(gè)單例。這表明,一旦創(chuàng)建,它就會(huì)一直存在直至整個(gè)應(yīng)用進(jìn)程被銷毀。由代碼可知, CrimeLab 引用著 mContext 對(duì)象。顯然,如果把 activity 作為 mContext 對(duì)象保存的話,這個(gè)由 CrimeLab 一直引用著的 activity 肯定會(huì)免遭垃圾回收器的清理,即便用戶跳轉(zhuǎn)離開(kāi)這個(gè) activity 時(shí)也是如此。
為了避免資源浪費(fèi),我們使用了應(yīng)用程序上下文。這樣, CrimeLab 仍可以引用 Context 對(duì)象, 而 activity 的存在和消亡也不用受它束縛了。
7 挑戰(zhàn)練習(xí):刪除 Crime 記錄
在上一章中,我們?yōu)?CrimeFragment 的 ToolBar 中添加了刪除按鈕,這一章我們改用數(shù)據(jù)庫(kù)來(lái)存儲(chǔ)數(shù)據(jù),需要對(duì)代碼進(jìn)行相應(yīng)的改寫:
// CrimeLab.java
public void deleteCrime(Crime c) {
mDatabase.delete(
CrimeTable.NAME,
Cols.UUID + " = ?",
new String[] {c.getId().toString()}
);
}
// CrimeFragment.java
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_delete_crime:
CrimeLab.get(getActivity()).deleteCrime(mCrime);
getActivity().finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
GitHub Page: kniost.github.io
簡(jiǎn)書(shū):http://www.itdecent.cn/u/723da691aa42