8、ContentValues与游标:数据封装与遍历的艺术
说实话,ContentValues和Cursor这两个东西,是我刚入行Android时最早接触的数据库API。那时候觉得它们就是简单的数据容器和遍历工具,用久了才发现——这里面的门道还真不少。
今天咱们就好好聊聊这两个核心组件。我保证,看完这一章,你写数据库代码会顺手很多。
8.1 ContentValues:数据封装的正确姿势
ContentValues说白了就是一个键值对容器。它的作用就是把你要插入或更新的数据打包好,然后交给SQLiteDatabase去执行。
我个人习惯用ContentValues来替代手写SQL的INSERT语句。为什么?因为安全。你想想看,手写SQL拼接字符串,稍不留神就是SQL注入漏洞。ContentValues内部做了参数化处理,安全得多。
// 创建一个ContentValues对象
ContentValues values = new ContentValues();
// 添加数据
values.put("name", "张三");
values.put("age", 28);
values.put("email", "zhangsan@example.com");
values.put("is_active", 1);
// 执行插入
long newRowId = db.insert("users", null, values);
if (newRowId != -1) {
Log.d("DB", "插入成功,新行ID: " + newRowId);
}
这里有个细节我要提醒你——put()方法的第二个参数是Object类型,但实际支持的数据类型有限。我在项目中遇到过有人传了一个Date对象进去,结果存到数据库里变成了字符串,读取时还得自己解析。嗯,这是个坑。
另外,批量插入时,我习惯复用同一个ContentValues对象,每次清空再填充。这样可以减少对象创建的开销。
// 批量插入的推荐做法
ContentValues values = new ContentValues();
db.beginTransaction();
try {
for (User user : userList) {
values.clear();
values.put("name", user.getName());
values.put("age", user.getAge());
db.insert("users", null, values);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
8.2 Cursor游标:数据遍历的核心工具
Cursor是什么?你可以把它想象成一个指向结果集某一行数据的指针。查询数据库返回的不是一个List,而是一个Cursor。你得手动移动这个指针,才能拿到每一行的数据。
我第一次用Cursor时犯了个低级错误——忘了调用moveToFirst()就直接去读数据,结果程序直接崩溃。后来我养成了一个习惯:每次拿到Cursor,第一件事就是检查它是不是空的,然后移动到第一行。
Cursor cursor = db.rawQuery("SELECT * FROM users", null);
// 标准的三步走
if (cursor != null && cursor.moveToFirst()) {
do {
// 读取当前行的数据
String name = cursor.getString(cursor.getColumnIndex("name"));
int age = cursor.getInt(cursor.getColumnIndex("age"));
Log.d("DB", "用户: " + name + ", 年龄: " + age);
} while (cursor.moveToNext());
}
// 别忘了关闭
cursor.close();
8.3 Cursor常用方法详解
Cursor提供了很多方法,但日常开发中常用的就那么几个。我整理了一个表格,方便你查阅。
| 方法 | 作用 | 返回值 |
|---|---|---|
| moveToFirst() | 移动到第一行 | boolean(成功返回true) |
| moveToNext() | 移动到下一行 | boolean(还有下一行返回true) |
| moveToPosition(int) | 移动到指定行 | boolean |
| moveToLast() | 移动到最后一行 | boolean |
| getString(int) | 获取当前行指定列的字符串值 | String |
| getInt(int) | 获取当前行指定列的整数值 | int |
| getLong(int) | 获取当前行指定列的长整数值 | long |
| getFloat(int) | 获取当前行指定列的浮点数值 | float |
| getDouble(int) | 获取当前行指定列的双精度值 | double |
| getColumnIndex(String) | 根据列名获取列的索引 | int(-1表示列不存在) |
| getColumnName(int) | 根据索引获取列名 | String |
| getCount() | 获取结果集的行数 | int |
| isClosed() | 判断Cursor是否已关闭 | boolean |
| close() | 关闭Cursor,释放资源 | void |
这里我要特别说说getColumnIndex()。这个方法返回-1时,意味着你传入的列名在结果集中不存在。我曾经因为拼写错误,把"username"写成了"user_name",结果getColumnIndex返回-1,getString直接抛异常。所以我的习惯是:
int nameIndex = cursor.getColumnIndex("name");
if (nameIndex != -1) {
String name = cursor.getString(nameIndex);
} else {
Log.w("DB", "列 'name' 不存在");
}
8.4 Cursor关闭与资源释放
这是很多新手容易忽略的问题。Cursor使用完后必须关闭,否则会泄漏数据库连接。Android的Cursor是持有数据库引用的,不关闭的话,数据库文件可能无法被删除或重命名。
我见过最严重的案例:一个同事在Activity里查询了数据,但忘了关闭Cursor。结果那个Activity被销毁后,数据库文件还被占用着,导致后续的数据库升级操作全部失败。排查了半天才发现是Cursor泄漏。
正确的做法是:
Cursor cursor = null;
try {
cursor = db.query("users", null, null, null, null, null, null);
while (cursor.moveToNext()) {
// 处理数据
}
} catch (Exception e) {
Log.e("DB", "查询失败", e);
} finally {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
如果你用的是Kotlin,可以用use扩展函数自动关闭:
db.query("users", null, null, null, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
val name = cursor.getString(cursor.getColumnIndex("name"))
// 处理数据
}
// 自动关闭
}
8.5 知识体系总览
下面这张图展示了ContentValues和Cursor在整个数据库操作中的位置和关系。我画这张图时特意把数据流向标清楚了,方便你理解。
嗯,这张图把今天讲的内容串起来了。你写代码时,脑子里要有这个流程:先用ContentValues把数据打包好,插入数据库;查询时拿到Cursor,一步步遍历;最后别忘了关闭Cursor释放资源。
说白了,这两个工具都不复杂,但用不好就会出问题。我见过太多因为Cursor没关闭导致的内存泄漏,也见过因为ContentValues类型传错导致的诡异bug。希望今天的分享能帮你避开这些坑。
公众号:蓝海资料掘金营,微信deep3321