方法的使用:用 TaoToken 统一 Key 跑通 SQLiteDatabase 查询链路)
1. rawQuery 查询链路为什么总在真机上翻车rawQuery()是SQLiteDatabase上执行select语句最直接的入口它把 SQL 文本和参数数组交给 SQLite 引擎返回一个Cursor游标。你能用它做条件筛选、模糊匹配、聚合统计甚至多表联查。适合谁适合已经用SQLiteOpenHelper建好库表、想精确控制 SQL 又不想被query()那套参数拼装绕晕的 Android 开发者。但我在实际项目里见过太多「模拟器跑得好好的真机一查就崩」的案例。问题往往不在 SQL 本身而在整条链路的细节占位符写成了字符串拼接、Cursor用完没关、moveToFirst()返回 false 还硬读列、异常抛出后数据库连接泄漏。这些坑单看都不大凑在一起就是线上偶发的CursorWindow报错和内存增长。这篇就围绕rawQuery、SQLiteDatabase、select、Cursor四个点把从建库、传参到游标遍历的完整调用路径拆开讲。每一步都给可复制的代码片段再配逐项验证动作。另外我会用 TaoToken 的统一 Key 来跑通模型侧的辅助校验——比如让模型帮我审查 SQL 拼接风险、生成边界测试用例这样查询链路的验证不用全靠人肉盯。先说清楚 TaoToken 在这里的角色它是一个统一的大模型 API 接入层你拿一个 Key 就能调用多种模型用来做代码审查、SQL 风险扫描、测试用例生成这类辅助工作。它不替代你的 SQLite也不碰你的本地数据库只是帮你把「写查询、验查询」这件事做得更稳。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。为什么查询链路需要模型辅助因为rawQuery的坑大多是「语义级」的占位符和参数数量对不上、like的通配符位置写错、Cursor列索引和select字段顺序错位。这些编译器不报错运行时才炸。让模型按你的表结构做一轮静态审查能提前拦掉一大半。我试过把一段带like ?的查询丢给模型它直接指出参数数组里%iteedu%的通配符应该由调用方拼接而不是写死在 SQL 里否则占位符就失去意义了。这种细节人眼扫过去很容易漏。所以这篇的结构是先讲清rawQuery的调用路径和常见翻车点再给出 TaoToken 的前置配置然后是可直接复制的查询代码和 Cursor 关闭配置接着是验证请求和成功结果最后是报错排查和 CTA。你可以按顺序跟做也可以直接跳到配置章节抄代码。2. TaoToken 前置配置拿 Key、选模型、配 Base URL在开始写查询代码之前先把 TaoToken 的接入配好。这一步是为了后面能用模型辅助审查 SQL 和生成测试用例。整个配置就三件事拿 API Key、选模型、配 Base URL。2.1 获取 API Key打开 https://taotoken.net/api-keys 登录后创建一个新的 Key。建议按用途命名比如sqlite-rawquery-review方便后面区分。创建完立刻复制页面刷新后就看不到了。拿到 Key 之后先做一次最小验证确认 Key 可用curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: 回复 ok}], max_tokens: 16 }返回里能看到choices[0].message.content就说明 Key 通了。如果返回 401先检查 Key 有没有复制完整、有没有多余空格。2.2 选模型TaoToken 支持多种模型做代码审查我一般用 Claude 系列对 SQL 语义和边界条件比较敏感。你可以在模型对话页面 https://taotoken.net/chat 里先手动试几轮确认效果再写进代码。2.3 配置 Base URL 和 Model ID如果你用 Cline、CC Switch 这类工具配置项就三个Base URL、API Key、Model ID。以 Cline 的 MCP 配置为例写进settings.json{ mcpServers: { taotoken: { command: npx, args: [-y, taotoken/mcp-server], env: { TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_API_KEY: sk-你的Key, TAOTOKEN_MODEL_ID: claude-sonnet-4-20250514 } } } }注意 Base URL 是https://taotoken.net/api不要加 UTM 参数也不要写成带/v1的完整路径——SDK 会自己拼。Model ID 要和你在模型对话里验证过的一致。如果你用 Codex配置写在auth.json里{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-20250514 }三件套齐了Base URL、Key、Model ID。缺一个都会报local proxy failed或 401。2.4 验证配置配完跑一次真实请求确认模型能返回内容curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: 下面这段 SQL 有注入风险吗select * from person where name like ?}], max_tokens: 256 }能拿到分析结果前置配置就算完成了。接下来进入查询代码本身。3. 可复制配置rawQuery 查询代码与 Cursor 关闭这一章是核心给出从建库到游标遍历的完整代码。你可以直接复制到项目里改表名和字段。3.1 建库建表先用SQLiteOpenHelper建库。表结构简单点一个人名表public class PersonDbHelper extends SQLiteOpenHelper { private static final String DB_NAME person.db; private static final int DB_VERSION 1; public PersonDbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE person ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER NOT NULL)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(DROP TABLE IF EXISTS person); onCreate(db); } }3.2 无占位符的 rawQuery最简单的查询第二个参数传nullSQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.rawQuery(select * from person, null); try { while (cursor.moveToNext()) { long id cursor.getLong(cursor.getColumnIndexOrThrow(id)); String name cursor.getString(cursor.getColumnIndexOrThrow(name)); int age cursor.getInt(cursor.getColumnIndexOrThrow(age)); Log.d(rawQuery, id id , name name , age age); } } finally { cursor.close(); db.close(); }注意getColumnIndexOrThrow比getColumnIndex更安全列名写错会直接抛异常而不是返回 -1 让你在后面读到脏数据。3.3 带占位符的 rawQuery占位符用?参数按顺序放进String[]SQLiteDatabase db helper.getReadableDatabase(); String sql select * from person where name like ? and age ?; String[] args new String[]{%iteedu%, 4}; Cursor cursor db.rawQuery(sql, args); try { while (cursor.moveToNext()) { String name cursor.getString(cursor.getColumnIndexOrThrow(name)); int age cursor.getInt(cursor.getColumnIndexOrThrow(age)); Log.d(rawQuery, name name , age age); } } finally { cursor.close(); db.close(); }这里有个关键点like的通配符%要放在参数值里不能写进 SQL 的?位置。写成where name like ?加参数%iteedu%是对的写成where name like %?%就错了占位符会被当成普通字符。3.4 Cursor 关闭与异常处理配置Cursor是资源必须关。推荐用 try-finally 或 try-with-resourcesAPI 16 的Cursor实现了CloseableSQLiteDatabase db helper.getReadableDatabase(); String sql select id, name, age from person where age ?; try (Cursor cursor db.rawQuery(sql, new String[]{3})) { if (cursor.moveToFirst()) { do { long id cursor.getLong(0); String name cursor.getString(1); int age cursor.getInt(2); Log.d(rawQuery, id / name / age); } while (cursor.moveToNext()); } } finally { db.close(); }用列索引getLong(0)的前提是你select的字段顺序和索引一致。如果select *字段顺序由表结构决定容易错位建议显式列出字段。3.5 用 TaoToken 审查这段 SQL把上面的 SQL 和表结构丢给模型让它检查占位符数量和参数数组是否匹配、有没有注入风险、like通配符位置对不对。请求体{ model: claude-sonnet-4-20250514, messages: [ { role: user, content: 表结构person(id INTEGER, name TEXT, age INTEGER)。SQLselect * from person where name like ? and age ?。参数[\%iteedu%\, \4\]。请检查占位符与参数是否匹配、通配符位置是否正确、有无注入风险。 } ], max_tokens: 512 }模型会返回逐项分析。这一步能帮你提前发现参数数量对不上这类运行时才炸的问题。4. 验证请求与成功结果逐项确认查询链路代码写完不算完要逐项验证。我一般分四步建库验证、插入验证、查询验证、游标验证。4.1 建库验证调用helper.getWritableDatabase()后检查数据库文件是否生成SQLiteDatabase db helper.getWritableDatabase(); Log.d(rawQuery, db path db.getPath()); Log.d(rawQuery, db version db.getVersion());日志里能看到/data/data/你的包名/databases/person.db和版本号 1说明建库成功。4.2 插入验证插几条测试数据SQLiteDatabase db helper.getWritableDatabase(); db.execSQL(insert into person(name, age) values(?, ?), new Object[]{iteedu_a, 4}); db.execSQL(insert into person(name, age) values(?, ?), new Object[]{iteedu_b, 5}); db.execSQL(insert into person(name, age) values(?, ?), new Object[]{other, 6}); db.close();用execSQL的占位符形式插入避免字符串拼接。4.3 查询验证跑 3.3 的带占位符查询预期返回两条iteedu开头且 age4 的记录。实际只有iteedu_a符合因为iteedu_b的 age 是 5。日志应该输出nameiteedu_a, age4如果输出为空先检查参数数组是不是[%iteedu%, 4]再检查表里数据是否真的插进去了。4.4 游标验证验证Cursor的边界行为Cursor cursor db.rawQuery(select * from person where age ?, new String[]{100}); Log.d(rawQuery, count cursor.getCount()); Log.d(rawQuery, moveToFirst cursor.moveToFirst()); cursor.close();age 100没有匹配记录getCount()返回 0moveToFirst()返回 false。如果你在moveToFirst()返回 false 后还去getString()就会抛CursorIndexOutOfBoundsException。所以遍历前一定要判断moveToFirst()的返回值。4.5 用模型生成边界测试用例把表结构和查询条件丢给模型让它列出边界用例{ model: claude-sonnet-4-20250514, messages: [ { role: user, content: 表 person(id, name, age)。查询select * from person where name like ? and age ?。请列出 5 个边界测试用例覆盖空结果、null 参数、通配符位置错误、参数数量不匹配、特殊字符。 } ], max_tokens: 768 }模型会给出用例清单你照着写测试就行。这比人肉想边界快得多。5. 常见报错排查401、local proxy failed、reading choices、OAuth这一章对照真实报错逐个排查。分两类TaoToken 接入报错和 SQLite 查询报错。5.1 TaoToken 接入报错401 UnauthorizedKey 不对。检查Authorization: Bearer sk-xxx里的 Key 有没有复制完整、有没有多余空格、有没有过期。重新去 https://taotoken.net/api-keys 生成一个再试。local proxy failedBase URL 配错了。确认是https://taotoken.net/api不要带/v1不要带 UTM 参数。如果你在 Cline 的settings.json里配检查TAOTOKEN_BASE_URL的值。reading choices 报错通常是响应体解析失败。先看原始返回curl -i -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d {model:claude-sonnet-4-20250514,messages:[{role:user,content:hi}],max_tokens:16}如果返回的是 HTML 而不是 JSON说明请求打到了错误的地址。检查 Base URL 和路径拼接。OAuth 报错如果你用 Claude Code 接入OAuth 流程走不通时改用 API Key 方式。在配置里填 Base URL、Key、Model ID 三件套不要走 OAuth。5.2 SQLite 查询报错no such column列名写错。用getColumnIndexOrThrow能提前暴露。检查select里的字段名和表结构是否一致。bind or column index out of range占位符数量和参数数组长度不匹配。数一下 SQL 里有几个?参数数组就该有几个元素。CursorIndexOutOfBoundsException游标位置越界。遍历前判断moveToFirst()遍历中用moveToNext()的返回值控制循环。CursorWindowAllocationException查询结果集太大CursorWindow 装不下。加limit分页或者只select需要的字段。5.3 排查顺序先确认 TaoToken 的 Key 和 Base URL 没问题再确认 SQL 本身能跑最后确认 Cursor 的遍历和关闭。顺序反了会在错误的地方浪费时间。6. 把 rawQuery 链路固化下来从验证到长期编码查询链路跑通之后下一步是把它固化。我一般做三件事把 SQL 审查加进编码流程、把边界测试用例沉淀成测试代码、把 Cursor 关闭写成模板。如果你长期做 Android 本地数据这块建议用 Coding Plan 把模型辅助审查变成日常动作。地址是 https://taotoken.net/coding-plan 。它适合需要反复审查 SQL、生成测试用例、排查游标问题的场景。接入文档在 https://taotoken.net/doc 里面有各语言 SDK 的调用示例。模型对话入口在 https://taotoken.net/chat 你可以先手动试几轮确认模型对 SQL 审查的效果再写进流程。最后给一个实用技巧把rawQuery的调用封装成一个方法统一处理 Cursor 关闭和异常public T ListT queryList(String sql, String[] args, RowMapperT mapper) { SQLiteDatabase db helper.getReadableDatabase(); ListT result new ArrayList(); try (Cursor cursor db.rawQuery(sql, args)) { while (cursor.moveToNext()) { result.add(mapper.map(cursor)); } } finally { db.close(); } return result; }这样每次查询不用重复写 try-finallyCursor 关闭也不会漏。配合模型审查 SQL整条链路就稳了。