Android編程權(quán)威指南(第二版)學(xué)習(xí)筆記(十四)—— 第14章 SQLite 數(shù)據(jù)庫(kù)

本章主要講了如何使用 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ò)如下步驟:

  1. 確認(rèn)目標(biāo)數(shù)據(jù)庫(kù)是否存在。
  2. 如果不存在,首先創(chuàng)建數(shù)據(jù)庫(kù),然后創(chuàng)建數(shù)據(jù)庫(kù)表以及必需的初始化數(shù)據(jù)。
  3. 如果存在,打開(kāi)并確認(rèn)數(shù)據(jù)庫(kù)架構(gòu)是否為最新版本
  4. 如果是舊版,就運(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容