一、SQLiteOpenHelper概念
SQLiteOpenHelper是android系統提供的用於創建及操作數據庫的工具類。該類中提供了創建、升級、降級時的回調方法,對應onCreate(),onUpgrade(),onDowngrade()。在使用時可以根據app功能進行覆寫相應的方法,從而實現正確的存儲數據。
二、SQLiteOpenHelper對象的構造
由於SQLiteOpenHelper是abstract類型的,所以在使用時必需繼承此類,並實現onCreate()和onUpgrade()抽象方法。其中onDowngrade()方法已經默認實現了,其具體的邏輯只是拋出一個異常說明不能降級數據庫。
SQLiteOpenHelper的構造方法主要有以下三個(android版本不同,提供的構造方法也不同,但主要是如下方法的變種),如下:
1、public SQLiteOpenHelper(@Nullable Context context, @Nullable String name, @Nullable CursorFactory factory, int version)
2、public SQLiteOpenHelper(@Nullable Context context, @Nullable String name, @Nullable CursorFactory factory, int version, @Nullable DatabaseErrorHandler errorHandler)
3、public SQLiteOpenHelper(@Nullable Context context, @Nullable String name,@Nullable CursorFactory factory, int version,int minimumSupportedVersion, @Nullable DatabaseErrorHandler errorHandler)
在構造方法上,有一段如下的說明註釋
Create a helper object to create, open, and/or manage a database.This method always returns very quickly. The database is not actually created or opened until one of {@link #getWritableDatabase} or {@link #getReadableDatabase} is called.
從中可知,構造SQLiteOpenHelper並不會直接生成具體的數據庫文件。
在構造方法中,都包含了name和version這兩個參數。即數據庫的文件名和數據庫的版本。在處理數據庫的升級或降級時需手動的修改version這個參數值。源代碼中,name參數的說明如下:
name of the database file, or null for an in-memory database
如果name傳入的是null,那麼系統會將數據庫存在內存中。當應用被殺死的時候,數據庫的內容也會被丟失。
1、CursorFactory
CursorFactory類是一個接口類,該類用於創建一個Cursor的子類。如果想實現自已的cursor邏輯,那麼則可以傳入自定義的CursorFactory對象。如果傳入null,那麼SQLiteOpenHelper將使用默認的cursor,即SQLiteCursor。
2、DatabaseErrorHandler
DatabaseErrorHandler定義了當數據庫發生損毀時執行的操作。類說明如下:
An interface to let apps define an action to take when database corruption is detected.
系統提供了默認的實現類,DefaultDatabaseErrorHandler。一般情況使用默認的即可。代碼如下:
mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler();
默認的操作是當數據庫發生損毀時,刪除相關的數據及日誌文件。具體見源代碼實現。
三、onCreate()方法分析
onCreate()方法不是在類加載的時候調用,而是在程序調用getWritableDatabase/getReadableDatabase的時候調用的。有篇博客說這個正好可以解釋SQLiteOpenHelper類中爲什麼帶了open這個單詞。android中的一些變量及類的命名確實很精確和嚴謹。
SQLiteOpenHelper中getWritableDatabase的方法註釋部份如下:
Create and/or open a database that will be used for reading and writing.
The first time this is called, the database will be opened and
{@link #onCreate}, {@link #onUpgrade} and/or {@link #onOpen} will be called.
在onCreate()方法中,常常進行數據庫表的創建操作。通過參數SQLiteDatabase對象提供的execSQL()方法,執行sql語句。如下:
// 如下sql可用navicat工具創建好,在導出sql,複製粘貼到這裏即可。其中自增的字段要設置成主鍵,否則創建失敗。
var sqlStr = "CREATE TABLE user (\n" +
"\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n" +
"\"name\" TEXT NOT NULL,\n" +
"\"age\" INTEGER,\n" +
"\"phone\" TEXT\n" +
");"
try {
db?.execSQL(sqlStr) // sql 不能包含多條sql語句,只能單條。詳見方法說明。
} catch (e: Exception) {
e.printStackTrace()
}
其中數據庫的創建語句,可以利用可視化的工具(如navicat)創建,在導出sql語句,複製粘貼即可。 onCreate()方法成功執行後,則會生成user表。
注意,sqlite中設置了autoincrement的列必需要設置成not null,否則sql語句會執行失敗。
四、onUpgrade()方法分析
應用中調用了getWritableDatabase/getReadableDatabase方法的時候,如果應用沒有舊數據庫存在,則會回調onCreate(), 如果舊數據庫的版本號小於當前設置的版本號,則會調用此方法實現數據庫的升級邏輯。系統回調的代碼部份如下
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);
}
if (version > 0 && version < mMinimumSupportedVersion) {
File databaseFile = new File(db.getPath());
onBeforeDelete(db);
db.close();
if (SQLiteDatabase.deleteDatabase(databaseFile)) {
mIsInitializing = false;
return getDatabaseLocked(writable);
} else {
throw new IllegalStateException("Unable to delete obsolete database "
+ mName + " with version " + version);
}
} else {
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();
}
}
}
從代碼可以看出,當舊version與新version相同的時候,onCreate(), onUpgrade(), onDowngrade()方法都不會回調。 故可以推斷出,onCreate(), onUpgrade(), onDowngrade()這三個方法只會執行一次。
1、表結構的修改
數據庫升級時,往往有修改表結構的需求。如修改主鍵,增加列,改字段長度等。實現的方式有多種,可以採用創建新表,將舊錶的數據導入,也可以使用update語句修改。例如想在上面創建的user表的基礎上增加一列addr,採用創建新表的方式,代碼如下:
fun createTable2(db: SQLiteDatabase?) {
// version 1 to 2
// 將舊錶改名,數據都保存在這裏
var renameTableSql = "ALTER TABLE user RENAME TO user_backup"
db?.execSQL(renameTableSql)
Log.d(SH_TAG, "alter table name");
// 創建新表
// version 2 增加一個字段addr
var sqlStr = "CREATE TABLE user (\n" +
"\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n" +
"\"name\" TEXT NOT NULL,\n" +
"\"age\" INTEGER,\n" +
"\"phone\" TEXT,\n" +
"\"addr\" TEXT DEFAULT 武漢市,\n" +
"\"gender\" INTEGER DEFAULT 1\n" +
");"
db?.execSQL(sqlStr)
Log.d(SH_TAG, "alter table version2");
// 將舊錶數據導入新表, 新表的增加的字段有默認值。 這裏一定要注意默認值的問題,否則列數量不對等,容易出現問題。
var insertDataSql = "insert into user(name, age, phone) SELECT name, age, phone from user_backup"
db?.execSQL(insertDataSql)
Log.d(SH_TAG, "import data from old table");
// 刪除舊錶
var deleteOldTableSql = "DROP table user_backup"
db?.execSQL(deleteOldTableSql)
Log.d(SH_TAG, "delete old table");
}
採用update語句如下:
var addColumnSql = "alter table user add addr text DEFAULT '武漢'"
db?.execSQL(addColumnSql)
兩種方式,前者是重新創建並從舊錶導入數據,而後者只涉及修改,開銷比前者小。所以只在簡單的增加一列的情況下,儘量選擇後者。如果涉及複雜的修改,則建議用前者,這樣出錯的機率比較小。
2、跨版本升級時的注意事項
在處理升級數據庫時,一定要注意跨版本升級。當應用跨版本升級時,其跨過的數據庫版本的邏輯也要執行一次。如下代碼
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
Log.d(SH_TAG, "oldVersion is $oldVersion and newVersion is $newVersion");
when(newVersion) {
2 -> {
createTable2(db)
}
3 -> {
createTable2(db)
createTable3(db)
}
}
// version 2 to 3
// 數據庫版本升級要依次過每一個version。
// version 3 to 4
}
當數據庫版本從1升級到3時,其也要將數據庫版本2的升級邏輯執一次。注意,當前版本的onCreate()方法也要相應的修改,因爲用戶全新安裝時是從數據庫最新版本開始的,不會執行升級過程。
五、onDowngrade()方法分析
在實際中,遇到數據庫降級的情況不多。況且android正常安裝流程(adb安裝也要加入-d參數)是不允許安裝降級應用的。該方法系統的默認實現是拋出異常,如下:
throw new SQLiteException("Can't downgrade database from version " +
oldVersion + " to " + newVersion);
如果實現自已的降級邏輯,則可以利用sql語句刪除對應版本的邏輯。
六、數據庫的增刪改查
數據庫的增刪改查,可以通過SQLiteDatabase提供的execSQL()方法執行sql語句(該方式不能進行查詢操作,因爲execSQL是沒有返回值的)也可以通過SQLiteDatabase提供的增刪改查的方法。
1、執行原始sql語句
如下代碼對應增、刪、改。由於execSQL方法沒有返回值,故無法獲知sql語句是否執行成功。如果想要獲取執行狀態,則可以利用系統提供的標準接口。
var insertSql = "insert into user (name, age, phone, addr, gender) values(\"hahaha\", \"18\", \"13434323456\", \"武漢市\", 1)"
writeableDatabase.execSQL(insertSql)
var deleteSql = "delete from user where name like '%haha%'"
writeableDatabase.execSQL(deleteSql)
var updateSql = "update user set name = 'hello' where name like '%ha%'\n"
writeableDatabase.execSQL(updateSql)
2、通過提供的方法
查詢方法如下:
var cursor = writeableDatabase.query("user", arrayOf("name"), "name like ?", arrayOf("%張三%"), "name", null, null)
while (cursor != null && cursor.moveToNext()) {
var column = cursor.getColumnIndex("name")
Log.i("sh", "name is " + cursor.getString(column))
}
其中需要注意的是,where clause中不能帶%,%要放在selectionArgs中。
插入的方法如下:
var contentValues = ContentValues()
contentValues.put("name", "sh")
contentValues.put("age", "27")
contentValues.put("phone", "136****2001")
var rowId = writeableDatabase.insert("user", null, contentValues)
刪除的方法如下:
var rowId = writeableDatabase.delete("user", "name = ? or name = ?", arrayOf("張三丰", "李四"))
更新的方法如下:
var contentValues4 = ContentValues()
contentValues4.put("name", "hondar")
var rowId2 = writeableDatabase.update("user", contentValues4, "name = ?", arrayOf(""))
七、總結
1、數據庫是在調用SQLiteOpenHelper的getWritableDatabase/getReadableDatabase方法時才創建的,而不是在new SQLiteOpenHelper的時候。
2、SQLiteOpenHelper的onCreate()、onUpgrade()、onDowngrade(),在一次安裝期間只會調用其中的一個方法。
3、在處理升級的時候,必需考慮到跨版本的升級。並有要考慮最新版本的新安裝的用記數據庫,即onCreate裏的數據庫初始化內容。