Android 自带的数据库系统是 SQLite,SQLite 是 C 和 C++ 实现,因此 Android 在 Framework 层封装了一层 Java 接口。主要的封装类型为 SQLiteOpenHelper、SQLiteDatabase 以及 Cursor。不管如何封装,最后都是将 SQL 语句提交到 SQLite 中执行。
1. SQLiteOpenHelper
该类封装了 SQLite 数据库的创建与升级等工作,通过该类也可以获取到实际操作数据库的 SQLiteDatabase 对象。
通常我们会继承 SQLiteOpenHelper 类,然后实现它的 onCreate、onUpgrade 函数,这两个函数分别在创建数据库、数据库升级时调用。因此可以在 onCreate 函数中创建表,在 onUpgrade 时升级数据库。
代码大致如下:
public class SQLiteDbHelper extends SQLiteOpenHelper {
public static final String DB_NAME = "database.db";
public static final int DB_VERSION = 1;
public SQLiteDbHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
// 构造函数中指定数据库名与版本号
super(context, DB_NAME, factory, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// 在这里通过 db.execSQL 函数执行 SQL 语句创建所需要的表
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 数据库版本号变更会调用 onUpgrade 函数,在这根据版本号进行升级数据库
switch (oldVersion) {
case 1:
// do something
break;
default:
break;
}
}
@Override
public void onOpen(SQLiteDatabase db) {
super.onOpen(db);
if (!db.isReadOnly()) {
// 启动外键
db.execSQL("PRAGMA foreign_keys = 1;");
//或者这样写
String query = String.format("PRAGMA foreign_keys = %s", "ON");
db.execSQL(query);
}
}
}
可以看到,每个方法都有一个 SQLiteDatabase 参数,所以真正操作数据库的类型不是 SQLiteOpenHelper,而是 SQLiteDatabase。
2. SQLiteDatabase
SQLiteDatabase 是 SQLite 数据库的管理类,提供了对 SQLite 数据库操作的接口,例如数据库的增、删、查、改。下面我们来看看它的核心接口。
2.1 插入数据
SQLiteDatabase 插入数据的接口为 insertWithOnConflict,它的声明如下:
public long insertWithOnConflict(String table, String nullColumnHack,
ContentValues initialValues, int conflictAlgorithm)
参数 1 表示要操作的表名,参数 2 当参数 3 为空时将表中的字段值置为 null,参数 3 为表中各字段的键值对(键的名字与表中的字段名要一致),参数 4 为当插入数据发生冲突时如何解决。
参数 4 conflictAlgorithm 的可选值有 6 种,这些策略是声明在 SQLiteDatabase 类中。
策略 | 说明 |
---|---|
CONFLICT_NONE | 忽略冲突 |
CONFLICT_ROLLBACK | 停止执行当前事务,并将数据恢复到操作前的状态 |
CONFLICT_ABORT | 停止执行当前事务,不过先前执行的操作造成的数据变动会被保留 |
CONFLICT_FAIL | 停止执行当前事务,不过先前执行的操作造成的数据变动会被保留 |
CONFLICT_IGNORE | 本次数据更改不会生效,但是后续的 SQL 语句会继续执行 |
CONFLICT_REPLACE | 当插入或者修改数据时违反了唯一性原则,新的数据会替换旧的数据 |
2.2 删除数据
public int delete(String table, String whereClause, String[] whereArgs)
delete 函数的参数 1 为表名,参数 2 为 where 条件,参数 3 为 where 条件语句中的字段值。其实参数 2 就是定义 column 限制条件的字符串,参数 3 则是定义参数的值。
下面我们来实现一个删除数据操作:
// 原生的 sql 语句
delete from students where name = 'Jerry' and cls_id = 2;
// 通过 delete 函数实现
db.delete("students", "name = ? and cls_id = ?",
new String[] {"Jerry", "2"});
whereClause 中的占位符 “?” 会被替换为 whereArgs 中的值,最终拼接为原生的 SQL 语句。
2.3 修改数据
public int updateWithOnConflict(String table, ContentValues values,
String whereClause, String[] whereArgs, int conflictAlgorithm)
参数 1 是表名,参数 2 是要更新的数据的键值对,参数 3 为 where 语句,参数 4 是 where 语句中的参数值,参数 5 是发生冲突时的处理策略。
2.4 查询数据
public Cursor query(String table, String[] columns, String selection,
String[] selectionArgs, String groupBy, String having,
String orderBy, String limit) {
参数名 | 作用 |
---|---|
table | 要操作的表名 |
columns | 要获取的字段数组 |
selection | 条件语句,也就是 where 那部分 |
selectionArgs | where 语句中的字段值 |
groupBy | 与 SQL 中的 group by 语句一样 |
having | Group by 中的 having 语句 |
orderBy | SQL 语句中的排序语句 |
limit | 限制返回的数据数量与偏移量 |
3. Cursor
当执行查询语句时,返回的数据类型为 Cursor。它是一个提供了随机读写访问数据库查询结果集的接口。Cursor 不是线程安全的,因此当在多线程中访问 Cursor 对象时要手动进行同步,避免出现线程安全问题。
常用的 Cursor 函数如下:
函数名 | 作用 |
---|---|
getString(index) | 通过字段的索引获取一个 String 类型的字段 |
getColumnIndex(String columnName) | 通过字段名查询该字段在数据库中的索引 |
getCount() | 在该 Cursor 中有多少条数据 |
MoveToFirst() | 将光标移到第一个数据的位置上 |
MoveToLast() | 将光标移到最后一个数据的位置上 |
MoveToNext() | 将光标移到下一个数据的位置上 |
IsClosed() | 判断光标是否已经关闭 |
IsFirst | 光标是否在第一个数据的位置上 |
IsLast() | 光标是否在最后一个数据的位置上 |
Close() | 关闭光标。在使用完 Cursor 之后一定要关闭光标。 |
- 通过 index 获取其他类型的数据与 getString(int index) 类似,只是函数名不一样,如 getInt(int index) 为获取整型的字段值。
- 通过索引获取字段值效率较高,因此,通常通过字段名获取索引 - getColumnIndex(String columnName),然后再通过索引获取字段值 - getString(int index)。
如何通过 Cursor 获取所有数据?
Cursor 中定义了一个光标位置,该光标默认指向第一个数据。用户可以通过 getString、getInt、getDouble 等函数获取到各个字段的值,然后通过 moveToNext 函数不断移动光标获取下一个数据,不断重复该过程就可以获取到所有数据。
以获取 students 表的数据为例子:
private void queryStudents() {
SQLiteDbHelper helper = new SQLiteDbHelper(getApplicationContext());
SQLiteDatabase database = helper.getWritableDatabase();
// 相当于 select * from students 语句
Cursor cursor = database.query("students", null, null, null,
null, null, null, null);
// 不断移动光标获取值
while (cursor.moveToNext()) {
// 直接通过索引获取字段值
int stuId = cursor.getInt(0);
// 先获取 name 的索引值,然后再通过索引获取字段值
String stuName = cursor.getString(cursor.getColumnIndex("name"));
Log.e("", "id: " + stuId + " name: " + stuName);
}
// 关闭光标
cursor.close();
}
4. Android 中的数据库事务
使用事务的两大好处是原子提交和更优性能。SQLite 默认会为每个插入、更新操作创建一个事务,并且在每次插入、更新后立即提交。
例如:连续插入 1000 次数据,那么实际的执行过程是重复 1000 次:创建事务 —> 执行语句 —> 提交。
如果使用数据库的事务,那么这个过程就会优化为:创建事务 —> 执行 1000 条语句 —> 提交,这样创建事务和提交这个过程只做一次,这种一次性事务可以使性能大幅提升。尤其当数据库位于 SD 卡时,时间上能节省两个数量级左右。
Android 中使用数据库事务的代码一般如下:
private void beginTransaction() {
SQLiteDbHelper helper = new SQLiteDbHelper(getApplicationContext());
SQLiteDatabase database = helper.getWritableDatabase();
database.beginTransaction();
try {
// 这里执行数据库操作
// 如果执行成功则调用 setTransactionSuccessful 函数
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}