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 拼接有啥区别?

⚠️ 重要提醒: 永远不要用字符串拼接的方式构造 selection!用占位符 ? 配合 selectionArgs 传参。

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 表字段类型要匹配。

💡 小技巧: 如果 selection 里只有一个条件,selectionArgs 可以写成 new String[]{"值"},不要偷懒传 null。我曾经因为传 null 导致查询条件失效,排查了半天。

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 的参数。怎么办?

有两种常见做法:

  1. 在 URI 里加参数:比如 content://authority/table?limit=20&offset=0
  2. 在 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);
}
⚠️ 注意: 上面代码为了演示简化了。实际项目中,建议用 SQLiteQueryBuilder 来构建查询,更安全。另外,LIMIT 和 OFFSET 的值一定要做校验,防止注入。

6.6 知识体系总览

下面这张图帮你理清 query() 的核心逻辑:

query() 方法核心流程 客户端调用 query() 解析 URI 和参数 Provider 处理 投影(列过滤) 选择(WHERE 条件) 排序(ORDER BY) 分页(LIMIT + OFFSET)

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 映射。把基础打牢,后面写复杂查询就不慌了。


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