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支持的数据类型包括:String、Integer、Long、Float、Double、Boolean、byte[]。其他类型请先转换再存入。

另外,批量插入时,我习惯复用同一个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();
核心要点:Cursor的初始位置是在第一行之前。所以你必须先调用moveToFirst()或moveToNext(),才能访问第一行数据。

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' 不存在");
}
注意:getColumnIndex是大小写敏感的。SQLite的列名默认不区分大小写,但Cursor的列名映射是区分大小写的。建议统一用小写。

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"))
        // 处理数据
    }
    // 自动关闭
}
我的经验:在Android中,Cursor的关闭时机很重要。如果你在Activity中使用了Cursor,建议在onDestroy中关闭。如果是在Loader或ViewModel中,交给系统管理就好。不要重复关闭,会抛异常。

8.5 知识体系总览

下面这张图展示了ContentValues和Cursor在整个数据库操作中的位置和关系。我画这张图时特意把数据流向标清楚了,方便你理解。

ContentValues 与 Cursor 知识体系 ContentValues 数据封装 put(key, value) 添加数据 clear() 清空复用 支持类型:String/Int/Long/Boolean/byte[] ⚠ 避免传入Date等复杂对象 插入/更新 Cursor 游标遍历 moveToFirst() 移动到首行 moveToNext() 遍历下一行 getString/getInt/getColumnIndex ⚠ 使用后必须 close() 释放资源 核心原则:ContentValues 安全封装 → Cursor 安全遍历 → 及时释放资源

嗯,这张图把今天讲的内容串起来了。你写代码时,脑子里要有这个流程:先用ContentValues把数据打包好,插入数据库;查询时拿到Cursor,一步步遍历;最后别忘了关闭Cursor释放资源。

说白了,这两个工具都不复杂,但用不好就会出问题。我见过太多因为Cursor没关闭导致的内存泄漏,也见过因为ContentValues类型传错导致的诡异bug。希望今天的分享能帮你避开这些坑。


公众号:蓝海资料掘金营,微信deep3321