解析 SQLiteOpenHelper

小邁發表於2019-02-25

“SQLiteOpenHelper” 是一個用來管理資料庫的建立和版本管理的輔助類。它是一個抽象類,要使用它必須建立一個子類繼承 SQLiteOpenHelper,並實現 onCreate,onUpgrade 這兩個抽象方法。這樣,如果資料庫存在,它就會開啟;如果不存在,就會建立這個資料庫,並且如果必要的話會自動升級資料庫。為了確保資料庫始終處於一個合理的狀態,它會使用事務。它便於 ContentProvider 實現在第一次使用之前延遲開啟和升級資料庫,以避免資料庫升級時間過長阻塞應用的啟動。

構造方法

public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version) {
        this(context, name, factory, version, null);
}

public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version,
        DatabaseErrorHandler errorHandler) {
    if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version);

    mContext = context;
    mName = name;
    mFactory = factory;
    mNewVersion = version;
    mErrorHandler = errorHandler;
}複製程式碼

可以看到,第一個 4 個引數的構造方法,會呼叫第二個 5 個引數的構造方法,但 4 個引數的構造方法會很快的返回。構造方法的作用是建立一個輔助物件來建立、開啟、管理一個資料庫。需要注意的是如果沒有呼叫過 getWriteableDatabase 或者 getReadableDatabase 資料庫不會自動建立或者開啟。構造方法中的引數除了context 和 version 之外,其他引數可以為 null。當 name 為 null 時,會在記憶體中建立資料庫;如果CursorFactory 為 null,會使用一個預設的。

從 5 個引數的構造方法中,可以看到 version 不能小於 1 ,否則會丟擲非法引數異常。

onCreate

public abstract void onCreate(SQLiteDatabase db);複製程式碼

很明顯這是一個抽象方法,前面我們也說過了,它是繼承 SQLiteOpenHelper 必須要實現的方法之一。

第一次建立資料庫時呼叫,在這個方法中建立表和表的初始化。

onUpgrade

public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);複製程式碼

這也是一個抽象方法,它也是繼承 SQLiteOpenHelper 必須要實現的方法之一。

當資料庫需要升級時,會呼叫這個方法。應該使用這個方法來實現刪除表、新增表或者做一些需要升級新的策略版本的事情。此方法在事務中執行。如果丟擲異常,所有更改將自動回滾。

onDowngrade

public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    throw new SQLiteException("Can`t downgrade database from version " +
            oldVersion + " to " + newVersion);
}複製程式碼

當資料庫需要降級時呼叫。這個方法與 onUpgrade(SQLiteDatabase, int, int) 方法非常相似,它是在資料庫之前版本比當前的版本新的時候,才會被呼叫。但是這個方法不是抽象的,因此它不強制要求重寫。如果這個方法沒有被重寫,預設的實現會拒絕資料庫版本降級,並丟擲SQLiteException異常。這個方法是在事務中執行的。如果有異常被丟擲,所有的改變都會被回滾。

這裡我要說一下,這種情況是怎麼發生的,比如使用者當前資料庫版本號為 2,但是Ta又覆蓋安裝了一箇舊版本(版本號為1),這個時候資料庫版本就從 2 降到了 1,就會觸發 onDowngrade 方法了。

getDatabaseName

public String getDatabaseName() {
    return mName;
}複製程式碼

這個方法再簡單不過了,就是返回資料庫的名字,它和構造方法中傳入的名字是一樣的。

setWriteAheadLoggingEnabled

public void setWriteAheadLoggingEnabled(boolean enabled) {
    synchronized (this) {
        if (mEnableWriteAheadLogging != enabled) {
            if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) {
                if (enabled) {
                    mDatabase.enableWriteAheadLogging();
                } else {
                    mDatabase.disableWriteAheadLogging();
                }
            }
            mEnableWriteAheadLogging = enabled;
        }
    }
}複製程式碼

這個方法的作用是:啟用或禁用資料庫的預寫日誌。

預寫日誌不能用於只讀的資料庫,因此如果資料庫是以只讀的方式被開啟,這個標記值會被忽略。

原始碼的第 3 行 if (mEnableWriteAheadLogging != enabled) 的使用方法可以借鑑一下,這行程式碼從邏輯上是可有可無的,它的妙處在於減少了不必要的程式碼執行,提高了效率(如果設定的值和當前值相等,就不作任何操作)。

那麼這個預寫日誌到底是什麼鬼呢?

從第 5、6 行程式碼可以看到如果 enabled 的值為 true,就會呼叫 mDatabase.enableWriteAheadLogging();

呼叫此方法後,只要資料庫保持開啟,則並行執行查詢操作。用於並行執行查詢的連線的最大數目取決於裝置記憶體和其他屬性。如果一個查詢是事務的一部分,那麼它就在同一個資料庫控制程式碼上執行。

它通過開啟資料庫的多個連線併為每個查詢使用不同的資料庫連線來實現在同一資料庫中並行執行多個執行緒的查詢,同時資料庫的日誌模式也被更改以啟用寫入與讀取同時進行。

當預寫日誌沒有啟用時(預設狀態),同一時間在資料庫上讀取和寫入是不可能的。因為寫入執行緒會在修改資料庫之前隱式地獲取資料庫上的互斥鎖,以防止寫入操作完成之前其他執行緒讀取訪問資料庫。

相反,當啟用預寫日誌時,寫入操作會在分離的日誌檔案中進行,該檔案允許讀取同時進行。當資料庫正在進行寫入操作時,其他執行緒上的讀取操作將在寫入開始前會感知資料庫的狀態,當寫入操作完成後,其他執行緒上的讀取操作將感知資料庫的新狀態。

當資料庫同時被多個執行緒同時訪問和修改時,啟用預寫日誌是一個好辦法。然而,開啟預寫日誌功能,會比普通日記使用更多的記憶體,因為同一個資料庫有多個連線。因此,如果一個資料庫只有一個執行緒使用,或者優化併發不是很重要,那麼應該禁用預寫日誌功能。

getWritableDatabase 和 getReadableDatabase

public SQLiteDatabase getWritableDatabase() {
    synchronized (this) {
        return getDatabaseLocked(true);
    }
}複製程式碼

這個方法用於建立或開啟用於讀取和寫入的資料庫。第一次被呼叫時資料庫將被開啟,onCreate、onUpgrade、onOpen 將被呼叫。一旦成功開啟,資料庫將被快取,因此每次需要寫入資料庫時,都可以呼叫此方法(確保當不再需要對這個資料庫進行操作時,呼叫 close方法關閉)。許可權問題或磁碟已滿等問題可能會導致這個方法開啟資料庫失敗,但如果問題是固定的,多次嘗試可能會成功。資料庫升級可能需要很長時間,不應該從應用程式主執行緒呼叫此方法。

public SQLiteDatabase getReadableDatabase() {
    synchronized (this) {
        return getDatabaseLocked(false);
    }
}複製程式碼

這個方法用於建立或開啟資料庫。它和 getWriteableDatabase() 方法返回的是同一個物件,但是因為它只需要返回只讀的資料庫,所以不會有磁碟已滿等問題。

這兩個方法的程式碼都很簡單,呼叫了 getDatabaseLocked(true/false);下面來看一下這個方法。

getDatabaseLocked

private SQLiteDatabase getDatabaseLocked(boolean writable) {
    if (mDatabase != null) {
        if (!mDatabase.isOpen()) {
            // Darn!  The user closed the database by calling mDatabase.close().
            mDatabase = null;
        } else if (!writable || !mDatabase.isReadOnly()) {
            // The database is already open for business.
            return mDatabase;
        }
    }

    if (mIsInitializing) {
        throw new IllegalStateException("getDatabase called recursively");
    }

    SQLiteDatabase db = mDatabase;
    try {
        mIsInitializing = true;

        if (db != null) {
            if (writable && db.isReadOnly()) {
                db.reopenReadWrite();
            }
        } else if (mName == null) {
            db = SQLiteDatabase.create(null);
        } else {
            try {
                if (DEBUG_STRICT_READONLY && !writable) {
                    final String path = mContext.getDatabasePath(mName).getPath();
                    db = SQLiteDatabase.openDatabase(path, mFactory,
                            SQLiteDatabase.OPEN_READONLY, mErrorHandler);
                } else {
                    db = mContext.openOrCreateDatabase(mName, mEnableWriteAheadLogging ?
                            Context.MODE_ENABLE_WRITE_AHEAD_LOGGING : 0,
                            mFactory, mErrorHandler);
                }
            } catch (SQLiteException ex) {
                if (writable) {
                    throw ex;
                }
                Log.e(TAG, "Couldn`t open " + mName
                        + " for writing (will try read-only):", ex);
                final String path = mContext.getDatabasePath(mName).getPath();
                db = SQLiteDatabase.openDatabase(path, mFactory,
                        SQLiteDatabase.OPEN_READONLY, mErrorHandler);
            }
        }

        onConfigure(db);

        final int version = db.getVersion();
        if (version != mNewVersion) {
            if (db.isReadOnly()) {
                throw new SQLiteException("Can`t upgrade read-only database from version " +
                        db.getVersion() + " to " + mNewVersion + ": " + mName);
            }

            db.beginTransaction();
            try {
                if (version == 0) {
                    onCreate(db);
                } else {
                    if (version > mNewVersion) {
                        onDowngrade(db, version, mNewVersion);
                    } else {
                        onUpgrade(db, version, mNewVersion);
                    }
                }
                db.setVersion(mNewVersion);
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        }

        onOpen(db);

        if (db.isReadOnly()) {
            Log.w(TAG, "Opened " + mName + " in read-only mode");
        }

        mDatabase = db;
        return db;
    } finally {
        mIsInitializing = false;
        if (db != null && db != mDatabase) {
            db.close();
        }
    }
}複製程式碼

首先這是一個私有的方法。在這個方法中會根據各種情況建立或者開啟一個資料庫並賦值給區域性變數 db;之後會呼叫 onConfigure(db) 這個方法下面會介紹,預設是一個空方法,我們可以重寫此方法,對 db 進行配置;之後會對當前資料庫的版本與最新版本進行對比,如果 db 是隻讀的,會丟擲異常(不能對只讀資料庫進行版本的更新);如果 db 可寫,就開啟一個事務,進行版本更新;版本更新完畢後,會呼叫 onOpen(db) 方法,同樣這個方法下面會介紹,也是一個空方法,可以重寫此方法;最後,會關閉資料庫。

onConfigure

在配置資料庫連線時呼叫,從上面 getDatabaseLocked 方法中,我們可以看出 onConfigure() 在 onCreate(SQLiteDatabase), onUpgrade(SQLiteDatabase, int, int), onDowngrade(SQLiteDatabase, int, int), onOpen(SQLiteDatabase) 這些方法之前呼叫。在這個方法中不應修改資料庫,除非根據需要配置資料庫連線,啟用預寫日誌或外來鍵支援等功能。

onOpen

資料庫被開啟時,會呼叫這個方法。在升級資料庫之前,這個方法的實現應該呼叫 isReadOnly() 方法檢查資料庫是否是隻讀的。

此方法在配置資料庫連線和建立資料庫模式、升級或必要的降級之後呼叫。如果資料庫連線的配置必須以某種方式建立,升級,或降級,那麼在onConfigure方法中做這些事情。

簡書地址:www.jianshu.com/p/842609bfe…
CSDN:blog.csdn.net/xiaomai9498…