一、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里的数据库初始化内容。