6、查询操作详解:query() 方法参数、投影、选择、排序、分页实现
好,咱们今天来聊聊 ContentProvider 里最核心、也最常用的操作——查询。说白了,就是怎么把数据从 Provider 那头拿过来。
我刚开始接触 ContentProvider 时,觉得 query() 方法不就是传几个参数嘛,有啥难的?结果真到项目里,各种坑踩了个遍。今天我把这些经验掰开揉碎了讲给你听。
6.1 query() 方法的六个参数
先看方法签名:
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
嗯,这里其实还有个隐藏参数,后面会讲到。咱们先看这五个。
| 参数 | 含义 | 类比 SQL |
|---|---|---|
| uri | 要查询的数据 URI | FROM 表名 |
| projection | 要返回的列 | SELECT 列名 |
| selection | 过滤条件(带占位符) | WHERE 条件 |
| selectionArgs | 占位符的实际值 | WHERE 参数 |
| sortOrder | 排序规则 | ORDER BY |
我个人习惯把 selection 和 selectionArgs 看作一对。为什么这么设计?说白了就是为了防止 SQL 注入。你想想看,如果直接把用户输入拼到 selection 字符串里,那跟裸写 SQL 拼接有啥区别?
6.2 投影(Projection)—— 只拿你需要的
投影就是指定要返回哪些列。我见过不少新手直接传 null,意思是「把所有列都给我」。这在数据量小的时候没问题,但一旦表里有 BLOB 字段或者几十列,性能就崩了。
举个例子:
// 不好的写法:全列查询
Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
null, // 所有列
null, null, null);
// 好的写法:只取需要的列
String[] projection = new String[] {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME
};
Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
projection,
null, null, null);
我在项目中遇到过一个问题:某个页面加载联系人列表特别慢,查了半天发现是投影里没限制列,把整个 Contacts 表的所有字段都拉回来了。改成只取 _ID 和 DISPLAY_NAME 后,速度提升了 3 倍。
6.3 选择(Selection)—— 精确过滤
selection 参数就是 WHERE 子句,但要注意写法。占位符用 ? 表示,实际值按顺序放在 selectionArgs 里。
String selection = "name = ? AND age > ?";
String[] selectionArgs = new String[]{"张三", "18"};
这里有个坑:selectionArgs 必须是 String 数组。如果你要传数字,也得转成字符串。Provider 那边会帮你做类型转换,但前提是你的 SQLite 表字段类型要匹配。
6.4 排序(SortOrder)—— 控制顺序
sortOrder 就是 ORDER BY 子句的内容。比如:
String sortOrder = "create_time DESC, name ASC";
嗯,这里要注意:排序字段必须是表里真实存在的列,否则会抛异常。另外,如果数据量很大,排序会很慢。我建议在 Provider 的 SQLite 表上建好索引。
6.5 分页实现 —— 别一次拿太多
分页是查询里最容易被忽视的部分。很多新手直接 query() 一把梭,把所有数据都拉回来。如果数据量上万,UI 线程直接卡死。
Android 原生 ContentProvider 没有直接的分页 API,但我们可以用 LIMIT 和 OFFSET 来实现。不过要注意:query() 方法没有直接传 LIMIT 的参数。怎么办?
有两种常见做法:
- 在 URI 里加参数:比如 content://authority/table?limit=20&offset=0
- 在 Provider 的 query() 里解析:自己解析 URI 参数,拼到 SQL 里
我个人推荐第一种,因为更直观。看代码:
// 客户端请求
Uri uri = Uri.parse("content://com.example.provider/items")
.buildUpon()
.appendQueryParameter("limit", "20")
.appendQueryParameter("offset", "0")
.build();
// Provider 端解析
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
String limit = uri.getQueryParameter("limit");
String offset = uri.getQueryParameter("offset");
String sql = "SELECT * FROM items";
if (selection != null) {
sql += " WHERE " + selection;
}
if (sortOrder != null) {
sql += " ORDER BY " + sortOrder;
}
if (limit != null) {
sql += " LIMIT " + limit;
if (offset != null) {
sql += " OFFSET " + offset;
}
}
// 执行 rawQuery
return db.rawQuery(sql, selectionArgs);
}
6.6 知识体系总览
下面这张图帮你理清 query() 的核心逻辑:
6.7 避坑指南
最后,分享几个我踩过的坑:
- Cursor 没关闭:查询完一定要在 finally 块里 close(),否则内存泄漏。我建议用 try-with-resources 或者 Kotlin 的 use()。
- selectionArgs 类型不匹配:如果字段是 INTEGER,但传了字符串,SQLite 会尝试隐式转换。但有时候会出问题,最好在 Provider 里做类型校验。
- 分页 OFFSET 性能问题:OFFSET 越大越慢。如果数据量很大,建议用「游标分页」——记住最后一条的 ID,下次查比它大的。
- URI 参数被吞:有些 Provider 会忽略 URI 里的自定义参数。如果你自己写 Provider,记得在 query() 里主动解析。
核心总结:
- 投影:只取需要的列,别偷懒传 null
- 选择:用占位符,别拼接字符串
- 排序:建好索引,别让数据库做全表排序
- 分页:用 LIMIT + OFFSET,或者游标分页
好了,query() 的细节就这些。你想想看,其实 ContentProvider 的查询跟 SQL 查询本质是一样的,只是多了一层 URI 映射。把基础打牢,后面写复杂查询就不慌了。