尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

sqlite之query、rawQuery区别;moveToNext,moveToFirst区别:用TaoToken统一Key跑通Android本地库查询验证

sqlite之query、rawQuery区别;moveToNext,moveToFirst区别:用TaoToken统一Key跑通Android本地库查询验证 1. Android SQLite 查询踩坑现场query 与 rawQuery 到底该用哪个刚接触 Android 本地库的朋友十有八九会在SQLiteDatabase的查询接口上卡一下明明query()和rawQuery()都能拿到Cursor为什么官方文档里两个都留着更让人头大的是游标遍历moveToNext()和moveToFirst()看起来都能把指针挪到第一行那到底有什么区别混着用会不会漏数据这篇就聚焦 Android SQLite 里最容易被忽略的两组 APIquery与rawQuery的差异以及moveToNext与moveToFirst的差异。场景很具体——本地数据库读写建一张用户表分别用两种查询方式取数据再逐条对比游标的返回值和位置变化。为了让你能直接跑起来对照我会用 TaoToken 的统一 Key 和 API 通道辅助生成对照用例代码把每一步的验证动作都写清楚。适合谁看正在写 Android 本地存储、被 Cursor 游标绕晕、想搞清楚「拼 SQL」和「传参数」两种风格边界的开发者。读完你能拿到可复制的建表与查询代码、游标遍历配置以及一套逐条验证的动作清单。先说结论方向方便你带着问题往下看query()是 Android 帮你拼 SQL参数化更安全rawQuery()是你自己写完整 SQL灵活但容易拼错。游标初始位置是 -1moveToFirst()和第一次moveToNext()都能落到第一行但循环遍历时用moveToNext()才是标准姿势。下面逐层拆开。2. TaoToken 统一 Key 前置给对照用例准备一条稳定通道在动手写 SQLite 代码之前先把「辅助生成对照用例」这条链路搭好。我习惯用 TaoToken 的统一 Key 来跑这类代码生成和验证任务原因是它把多个模型的调用收敛到一个 API 入口Base URL 和 Key 固定切换模型只改 Model ID不用来回改配置。对于「生成一段 query 和 rawQuery 对照代码」这种小任务统一通道能省掉不少环境折腾。你需要准备三样东西Base URL、API Key、Model ID。Base URL 用https://taotoken.net/apiAPI Key 在控制台的 API Keys 页面创建Model ID 按你实际要用的模型填。这三件套是后面所有配置的基础缺一不可。创建 Key 的入口在这里https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。进去之后新建一个 Key复制出来保存好注意它只在创建时完整显示一次。如果你用的是 Claude Code 这类编码工具想让它在终端里帮你生成 SQLite 对照用例可以走 Coding Plan 通道配置方式是把 Base URL 指向 TaoToken 的 API 地址Key 填刚创建的Model ID 选你套餐里支持的模型。Coding Plan 的入口https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。这里要提醒一句TaoToken 是 API 通道不是编辑器替代品它负责把请求转发到模型代码最终还是落在你的 Android 工程里。别指望它帮你改 Gradle 配置那是 IDE 的活。配置完成后你可以先用模型对话页面发一条测试请求确认 Key 和通道是通的https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。发一句「用 Kotlin 写一个 SQLiteOpenHelper 建 user 表」能正常返回就说明通道没问题。这一步别跳过后面生成对照用例全靠它。3. 可复制配置建表、query 与 rawQuery 对照代码这一节是全文的技术核心给你可以直接粘进 Android 工程的代码。先建表再分别用query()和rawQuery()查询最后配上游标遍历。所有代码用 Kotlin 写Java 版本逻辑一致改一下语法即可。先看建表。用SQLiteOpenHelper建一张user表字段包含id、name、ageclass DbHelper(context: Context) : SQLiteOpenHelper(context, demo.db, null, 1) { override fun onCreate(db: SQLiteDatabase) { db.execSQL( CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER NOT NULL) ) db.execSQL(INSERT INTO user (name, age) VALUES (许强, 28)) db.execSQL(INSERT INTO user (name, age) VALUES (李雷, 31)) db.execSQL(INSERT INTO user (name, age) VALUES (韩梅梅, 26)) } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL(DROP TABLE IF EXISTS user) onCreate(db) } }建完表插三条数据方便后面观察游标位置。接下来是query()的用法。query()有多个重载最常用的是 7 参数版本val db dbHelper.readableDatabase val cursor db.query( user, // table arrayOf(id, name, age), // columns age ?, // selection arrayOf(27), // selectionArgs null, // groupBy null, // having age ASC // orderBy )注意selection里的?会被selectionArgs按顺序替换且selectionArgs必须是String数组数字也要写成字符串。这是query()帮你拼 SQL 的关键——你只传条件片段它负责组装。再看rawQuery()同样的查询你自己写完整 SQLval sql SELECT id, name, age FROM user WHERE age ? ORDER BY age ASC val cursor2 db.rawQuery(sql, arrayOf(27))rawQuery()的第一个参数是完整 SQL 字符串里面的?占位符同样被第二个参数按顺序替换。你也可以不用占位符直接拼val sql2 SELECT * FROM user WHERE name 许强 val cursor3 db.rawQuery(sql2, null)两种写法都能跑但直接拼字符串有注入风险也不利于复用。下面用一张表把两者的参数对照清楚维度query()rawQuery()SQL 组装框架帮你拼你自己写完整 SQL参数传递selection selectionArgssql 里的 ? selectionArgs拼写错误风险较低字段名传错会报错较高SQL 写错直接抛异常灵活性受限于参数结构支持 JOIN、子查询等复杂语句底层调用最终走 rawQueryWithFactory最终走 rawQueryWithFactory两者最后都调用rawQueryWithFactory(CursorFactory, String, String[], String, CancellationSignal)所以性能上没有本质差别差别在写法风格和出错概率。游标遍历配置单独说。查询拿到的Cursor初始位置是 -1指向第一条记录的前一个位置。所以第一次moveToFirst()或第一次moveToNext()都能落到第一行。标准遍历写法if (cursor.moveToFirst()) { do { val id cursor.getInt(cursor.getColumnIndexOrThrow(id)) val name cursor.getString(cursor.getColumnIndexOrThrow(name)) val age cursor.getInt(cursor.getColumnIndexOrThrow(age)) Log.d(SQLiteDemo, id$id name$name age$age) } while (cursor.moveToNext()) } cursor.close()moveToFirst()返回Boolean结果集为空返回false否则返回true并把指针移到第一行。moveToNext()把指针从当前行移到下一行移过最后一行返回false。循环里用moveToNext()是因为它天然适合「还有下一行就继续」的语义。4. 验证请求与成功结果逐条对比游标返回值和位置变化代码写完不算完得逐条验证。这一节给你一套可执行的动作清单分别跑query和rawQuery再对比moveToFirst与moveToNext的返回值和位置变化。你可以把下面的日志代码加进去观察输出。先验证query()的结果。用第 3 节的cursor打印游标位置和返回值Log.d(SQLiteDemo, before move, position${cursor.position}) val firstResult cursor.moveToFirst() Log.d(SQLiteDemo, moveToFirst$firstResult, position${cursor.position}) Log.d(SQLiteDemo, first row name${cursor.getString(1)}) val nextResult cursor.moveToNext() Log.d(SQLiteDemo, moveToNext$nextResult, position${cursor.position}) Log.d(SQLiteDemo, second row name${cursor.getString(1)})预期输出before move时position-1moveToFirst返回trueposition0moveToNext返回trueposition1。因为age 27过滤后有三条数据28、31、26 中 26 被过滤实际两条许强 28、李雷 31所以第二次moveToNext后position1再调一次会返回false。再验证rawQuery()的结果用cursor2跑同样的动作Log.d(SQLiteDemo, raw before move, position${cursor2.position}) val rawFirst cursor2.moveToFirst() Log.d(SQLiteDemo, raw moveToFirst$rawFirst, position${cursor2.position}) val rawNext cursor2.moveToNext() Log.d(SQLiteDemo, raw moveToNext$rawNext, position${cursor2.position})预期输出和query()完全一致因为两者底层走的是同一个方法结果集相同。这验证了「query 和 rawQuery 只是写法不同结果等价」这个结论。接着单独验证moveToFirst和moveToNext的差异。关键动作是查询后不调moveToFirst直接调moveToNext看能不能拿到第一行。val cursor4 db.rawQuery(SELECT * FROM user ORDER BY id ASC, null) Log.d(SQLiteDemo, fresh cursor position${cursor4.position}) val directNext cursor4.moveToNext() Log.d(SQLiteDemo, direct moveToNext$directNext, position${cursor4.position}) Log.d(SQLiteDemo, row name${cursor4.getString(1)})预期fresh cursor position-1direct moveToNexttrueposition0拿到的正是第一行「许强」。这说明初始位置 -1 时第一次moveToNext()等价于moveToFirst()。再验证空结果集的情况。查一个不存在的条件val emptyCursor db.rawQuery(SELECT * FROM user WHERE age 100, null) Log.d(SQLiteDemo, empty moveToFirst${emptyCursor.moveToFirst()}) Log.d(SQLiteDemo, empty moveToNext${emptyCursor.moveToNext()})预期两个都返回false因为结果集为空。这解释了为什么遍历前要先if (cursor.moveToFirst())判断——空集时直接进循环会拿不到数据。最后验证moveToLast和moveToPrevious的边界补全游标家族val cursor5 db.rawQuery(SELECT * FROM user ORDER BY id ASC, null) Log.d(SQLiteDemo, moveToLast${cursor5.moveToLast()}, position${cursor5.position}) Log.d(SQLiteDemo, moveToPrevious${cursor5.moveToPrevious()}, position${cursor5.position}) Log.d(SQLiteDemo, moveToPrevious again${cursor5.moveToPrevious()}, position${cursor5.position})预期moveToLast返回trueposition指向最后一行第一次moveToPrevious返回true往前一行再往前如果越过第一行则返回false。这套动作跑完游标的位置语义就彻底清楚了。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth跑对照用例的过程中报错基本集中在两类一类是 TaoToken 通道的接入错误一类是 SQLite 代码本身的错误。逐个对照排查。401 Unauthorized。这是最常见的接入错误说明 API Key 没带上、带错或已失效。检查三件套Base URL 是否为https://taotoken.net/apiKey 是否从 API Keys 页面正确复制注意别带多余空格Model ID 是否拼写正确。如果你用的是 Claude Code 或 Cline MCP检查配置文件里的ANTHROPIC_BASE_URL或对应字段是否指向 TaoToken 地址。401 几乎都是 Key 的问题重新生成一个再试。local proxy failed。这个报错通常出现在本地代理配置环节说明请求没能到达目标地址。排查方向确认 Base URL 没有多余路径确认网络能正常访问 API 域名确认没有残留的本地代理设置干扰。如果你在 Cline MCP 或 Codex 的auth.json里配置过旧的地址清掉重配。注意auth.json里如果同时存在多个 provider 配置确认当前启用的是 TaoToken 那条。reading choices 报错。这类错误一般出现在解析模型返回时说明返回结构不符合预期。常见原因是 Model ID 填错导致返回的不是标准对话结构或者请求体格式不对。检查你的请求 JSON 是否符合所选模型的格式要求Model ID 是否在套餐支持范围内。用模型对话页面单独发一条请求能正常返回就说明通道没问题问题在客户端解析。OAuth 相关报错。如果你在 Claude Code 里走的是 OAuth 登录流程报错通常和 token 过期或回调地址不匹配有关。改用 API Key 方式接入可以绕开这类问题把 Base URL 指向 TaoTokenKey 填 API KeyModel ID 填对应模型。三件套配齐后OAuth 流程就不需要了。SQLite 侧的常见错。no such column说明字段名拼错query()和rawQuery()都会报no such table说明建表没执行或数据库版本没更新CursorIndexOutOfBoundsException说明没判断游标位置就取列记得先moveToFirst()或moveToNext()再getString()。还有一个隐蔽的坑getColumnIndex()返回 -1 时不会抛异常取列会拿到错误数据建议用getColumnIndexOrThrow()。排查顺序建议先确认 TaoToken 通道通不通用模型对话页面测再确认 SQLite 代码逻辑对不对看日志里的 position 和返回值。两边分开定位别混在一起查。6. 语义一致 CTA把对照用例跑成你自己的验证习惯到这里query与rawQuery、moveToNext与moveToFirst的差异应该已经清楚了。核心就三句话query()帮你拼 SQL 更安全rawQuery()自己写 SQL 更灵活两者底层等价游标初始位置 -1moveToFirst()和第一次moveToNext()都能落到第一行遍历用moveToNext()取首行用moveToFirst()空集判断别省。如果你想把这类对照用例固化成自己的验证习惯可以继续用 TaoToken 的统一 Key 通道生成更多场景的测试代码。接入和排障相关的文档在这里https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite API Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。需要长期跑编码和 Agent 任务的话Coding Plan 通道更适合https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。最后留一个我踩过的坑Cursor用完一定要close()否则在频繁查询的场景下会累积资源泄漏日志里不一定报错但内存会慢慢涨。把close()放在finally块里或者用use {}包起来养成习惯比事后排查省事得多。
返回列表