news 2026/10/8 6:14:27

Android SQLite query 实战:从 rawQuery 到参数化查询的完整配置与验证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android SQLite query 实战:从 rawQuery 到参数化查询的完整配置与验证

1. Android SQLite query 实战:从 rawQuery 到参数化查询的完整配置与验证

Android 本地数据库开发里,SQLiteDatabase.query和rawQuery是最常被拿来对比的两个方法。query是 Android 封装好的结构化查询接口,把表名、列名、条件、排序、分页拆成独立参数,内部帮你拼 SQL;rawQuery则允许你直接写完整 SQL 语句,自由度更高但更容易踩注入和拼接的坑。这篇文章面向正在写本地存储、缓存、离线数据的 Android 开发者,尤其是刚接触selectionArgs参数化写法、搞不清Cursor什么时候该关的人。我会把两种写法的选型逻辑、可直接复制的查询封装、注入风险对照表,以及用 adb 和单元测试验证结果的具体动作全部走一遍。你跟着敲完,至少能搞清楚三件事:什么时候用query、selectionArgs里的?到底怎么填、Cursor和SQLiteDatabase谁该在finally里关。

先说结论方向:绝大多数业务查询用query就够了,只有涉及多表 JOIN、子查询、GROUP BY 复杂聚合时,才轮到rawQuery上场。而无论用哪个,参数化都是底线,字符串拼接WHERE name='"+input+"'这种写法在本地库同样危险,别以为数据不出手机就没事。

2. TaoToken 前置:给查询结果加一层模型校验与语义检查

写数据库查询本身不需要联网,但当你需要验证「查出来的这批数据语义对不对」「字段映射有没有错位」「这条 SQL 的业务含义是否符合预期」时,接一个大模型做辅助检查会省很多事。我这边用的是 TaoToken,它的官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api ,兼容 OpenAI 风格的调用方式,Android 端用 OkHttp 或 Retrofit 都能直接发请求。

为什么在 SQLite 查询场景里要提它?因为实际开发中,Cursor遍历出来的Map经常出现字段名和值对不上、getColumnIndex返回 -1、null被吞成空串这类问题。你可以把查询结果 JSON 丢给模型,让它帮你核对字段结构,或者把一段rawQuery的 SQL 交给它做注入风险审查。这不是必须步骤,但对排查「数据看着对、逻辑就是不对」的情况很有用。

接入前你需要准备三样东西:Base URL、API Key、Model ID。Base URL 填https://taotoken.net/api,API Key 在控制台生成,地址是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。Model ID 根据你选的模型填,比如对话类模型直接写模型名即可。想先试对话效果,可以打开 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 看看可用列表。

如果你打算长期在 Android 项目里做编码辅助、Agent 调用,可以考虑 Coding Plan,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有完整的请求示例。Claude Code 相关的接入说明在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite 。

需要强调的是,TaoToken 在这里的角色是「查询结果的语义校验助手」,不是数据库本身,也不替代你的 SQLite 逻辑。数据库该在本地跑还是在本地跑,模型只是帮你多一双眼睛看数据。

3. 可复制配置:query 与 rawQuery 的封装代码与参数化写法

这一节是核心,直接给能跑的代码。先看query的标准签名,它有多个重载,最全的那个长这样:

public Cursor query(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)

参数含义逐个说清楚:distinct是否去重;table表名;columns要返回的列,传null表示所有列;selection是 WHERE 条件,用?占位;selectionArgs是占位符对应的值数组;后面四个是分组、过滤、排序、分页。注意selection里写_id=?,selectionArgs就传new String[]{"2"},顺序必须一一对应。

下面是我实际项目里用的查询封装,返回List<Map<String,String>>,比原 excerpt 里返回单条Map更通用:

public List<Map<String, String>> queryPersons(String selection, String[] selectionArgs) { List<Map<String, String>> result = new ArrayList<>(); SQLiteDatabase db = null; Cursor cursor = null; try { db = helper.getReadableDatabase(); cursor = db.query( true, // distinct "person", // table null, // columns,null 表示全部 selection, // 例如 "_id=?" selectionArgs, // 例如 new String[]{"2"} null, null, // groupBy, having "_id ASC", // orderBy "100" // limit ); int colsLen = cursor.getColumnCount(); while (cursor.moveToNext()) { Map<String, String> row = new HashMap<>(); for (int i = 0; i < colsLen; i++) { String colName = cursor.getColumnName(i); String colValue = cursor.getString(i); row.put(colName, colValue == null ? "" : colValue); } result.add(row); } } catch (Exception e) { Log.e("PersonDao", "query failed", e); } finally { if (cursor != null) cursor.close(); if (db != null) db.close(); } return result; }

这里有个细节:cursor.getString(i)直接用列索引,比getColumnIndex(colName)再查一次更快,也避免列名拼错返回 -1 的坑。原 excerpt 里用getColumnIndex是能跑,但多一次查找。

再看rawQuery的参数化写法,复杂查询用它:

public List<Map<String, String>> rawQueryPersons(String minId) { List<Map<String, String>> result = new ArrayList<>(); SQLiteDatabase db = null; Cursor cursor = null; try { db = helper.getReadableDatabase(); String sql = "SELECT p._id, p.name, p.age FROM person p " + "WHERE p._id > ? ORDER BY p.age DESC LIMIT 50"; cursor = db.rawQuery(sql, new String[]{minId}); while (cursor.moveToNext()) { Map<String, String> row = new HashMap<>(); row.put("_id", cursor.getString(0)); row.put("name", cursor.getString(1)); row.put("age", cursor.getString(2)); result.add(row); } } finally { if (cursor != null) cursor.close(); if (db != null) db.close(); } return result; }

rawQuery的第二个参数同样是selectionArgs,SQL 里的?按顺序被替换。千万别写成"WHERE _id > " + minId,这就是注入入口。

如果你在 Android 项目里用 Gradle 管理依赖,SQLite 本身是系统内置的,不需要额外依赖。但如果你用 Room,配置会不一样,这里聚焦原生 API。下面是一个settings.gradle片段,确认你的项目用的是标准 Android 配置:

// settings.gradle pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } } rootProject.name = "SqliteQueryDemo" include ':app'

注入风险对照表,直接看:

写法是否安全说明
selection="_id=?"+selectionArgs安全参数化,推荐
selection="_id=" + id危险拼接,id 来自外部即可注入
rawQuery("... WHERE name='"+name+"'", null)危险引号拼接,经典注入
rawQuery("... WHERE name=?", new String[]{name})安全参数化
execSQL("DELETE FROM person WHERE _id="+id)危险删除场景同样要参数化

4. 验证请求与成功结果:adb 与单元测试双验证

代码写完不能只看编译通过,得验证查询结果真的对。两种方式:adb 命令行直接查库,和单元测试断言。

先说 adb。Android 的 SQLite 库文件在应用私有目录,需要 root 或run-as才能访问 debug 包。命令如下:

# 进入应用私有目录(debug 包可用 run-as) adb shell run-as com.example.sqlitequerydemo ls databases/ # 打开数据库 adb shell run-as com.example.sqlitequerydemo sqlite3 databases/person.db # 在 sqlite3 交互里执行 sqlite> .tables sqlite> SELECT * FROM person; sqlite> SELECT _id, name FROM person WHERE _id > 2 ORDER BY age DESC LIMIT 5;

如果设备没有sqlite3命令,可以先把库文件 pull 出来:

adb exec-out run-as com.example.sqlitequerydemo cat databases/person.db > /tmp/person.db sqlite3 /tmp/person.db "SELECT * FROM person;"

成功结果应该看到类似:

1|张三|25 2|李四|30 3|王五|28

再说单元测试。用 AndroidJUnit4 加 Robolectric 可以在 JVM 上跑 SQLite 查询测试,不用真机:

@RunWith(AndroidJUnit4.class) public class PersonDaoTest { private PersonDao dao; private SQLiteDatabase db; @Before public void setUp() { Context ctx = ApplicationProvider.getApplicationContext(); db = SQLiteDatabase.create(null); db.execSQL("CREATE TABLE person(_id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"); db.execSQL("INSERT INTO person VALUES(1,'张三',25)"); db.execSQL("INSERT INTO person VALUES(2,'李四',30)"); dao = new PersonDao(db); } @Test public void queryById_returnsCorrectRow() { List<Map<String, String>> rows = dao.queryPersons("_id=?", new String[]{"2"}); assertEquals(1, rows.size()); assertEquals("李四", rows.get(0).get("name")); assertEquals("30", rows.get(0).get("age")); } @Test public void rawQuery_minId_returnsFiltered() { List<Map<String, String>> rows = dao.rawQueryPersons("1"); assertEquals(1, rows.size()); assertEquals("李四", rows.get(0).get("name")); } }

跑./gradlew test,两个用例都绿,说明参数化查询和 rawQuery 都按预期工作。如果断言失败,先看Cursor是不是没moveToNext就取值,或者selectionArgs顺序错了。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

这一节把实际会撞到的报错列出来,对照解决。

401 Unauthorized:如果你在 Android 端调 TaoToken 做结果校验,返回 401 通常是 API Key 没带或带错。检查请求头Authorization: Bearer <你的Key>,Key 从 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 生成。注意 Key 不要硬编码进 APK,用 BuildConfig 或本地配置注入。

local proxy failed:这个报错一般出现在你本地配了代理但代理没起来,或者 Android 模拟器的网络代理设置和宿主机不一致。检查模拟器设置里的代理,或者干脆清掉代理直连。注意这里说的是开发环境的网络配置问题,不是让你去搞什么特殊网络工具,正常公司网络或家庭网络直连即可。

reading choices 相关报错:调用模型接口时如果返回体解析失败,报reading choices或类似字段缺失,先打印原始响应体看结构。常见原因是请求的 Model ID 写错,或者接口返回了错误对象而不是正常结构。确认 Model ID 和文档一致,文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。

OAuth 相关报错:如果你用 Claude Code 或某些 CLI 工具接入,报 OAuth 失败,检查三件套是否齐全:Base URL 填https://taotoken.net/api,Key 填对,Model ID 填对。Claude Code 的接入说明单独在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite 。三件套缺一个都会认证失败。

Cursor 相关坑:getColumnIndex返回 -1 导致getString(-1)抛异常,改用列索引;Cursor忘记close导致内存泄漏,务必在finally关;SQLiteDatabase每次getReadableDatabase后都close其实不推荐,因为它是连接池,频繁开关反而低效,通常让 helper 管理生命周期,只在明确不用时关。

selectionArgs 顺序错:selection="_id=? AND name=?",selectionArgs必须new String[]{id, name},顺序反了查不到数据还不报错,最难查。

6. 语义一致 CTA:把查询校验接进你的开发流

数据库查询写对只是第一步,把结果校验和 SQL 审查接进日常开发流,能少踩很多坑。你可以把rawQuery的 SQL 和查询结果 JSON 一起发给模型做语义核对,接口地址用 https://taotoken.net/api ,Key 在 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 生成。想先试模型对话效果,打开 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。长期做 Android 编码辅助或 Agent 集成,看 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。完整接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。

最后留一个我常用的实用技巧:在PersonDao里加一个debugQuery方法,把最终执行的 SQL 和selectionArgs拼成可读字符串打日志,出问题时一眼看出参数有没有对上。参数化查询的?在日志里显示不出来,手动替换成实际值再打印,排查效率翻倍。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 6:13:53

TPS259483AYWPR与R7FA8D1BHECBD构建智能电源路径保护系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 6:13:05

智能体工程化落地指南:从Demo到生产的关键技术与安全实践

1. Trending风向&#xff1a;智能体项目从"能跑"走向"能上生产"1.1 我观察到的这一波变化&#xff1a;从玩具Demo到工程化基础设施这阵子我每周都会刷一遍GitHub Trending&#xff0c;明显能感觉到一个风向变化&#xff1a;智能体相关的项目不再只是"…

作者头像 李华
网站建设 2026/10/8 6:11:42

如何共用Skill:把agents的skill-sync配置改到TaoToken统一管理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华