1. 从 execSQL 到 query():Android SQLite API 查询到底解决了什么问题
如果你写过 Android 本地存储,大概率经历过这个阶段:一开始用db.execSQL("select * from person")拼字符串,字段一多、条件一复杂,SQL 语句就开始满天飞,改一个列名要全局搜索替换,还容易漏掉引号导致语法错误。Android 在SQLiteDatabase上提供了一套 API 方法——insert、delete、update、query,把 SQL 语句拆成参数,让编译器帮你做一部分检查,也让代码更可读。
这篇是复习练习九,核心就一件事:用 API 方式完成 SQLite 的增删改查,并且把接口调试的 endpoint 统一走 TaoToken 的 Key 通道,这样你在 Android Studio、Postman、Cline 等多个工具之间切换时,不用反复改 Key、改地址。适合谁?适合已经会写SQLiteOpenHelper、但每次调试接口都要手动换配置的 Android 开发者,也适合正在复习数据库 API、想把「本地 SQLite 查询」和「远程接口调试」串起来练一遍的人。
先说清楚一个容易混淆的点:db.query()查的是本地 SQLite 数据库,它不经过网络;而 TaoToken 统一 Key 通道解决的是你在调试远程 API 接口时的鉴权与地址管理问题。两者在同一个练习里出现,是因为真实开发中你往往一边操作本地库,一边调后端接口做数据同步或校验。把这两条线放在一起练,能帮你建立「本地数据层 + 远程接口层」的完整心智模型。
query()方法的完整签名有七个参数,很多人只记得前四个,后面三个groupBy、having、orderBy直接传null。这没错,但你要知道它们对应 SQL 里的什么:
| 参数 | 对应 SQL 子句 | 传 null 的含义 |
|---|---|---|
| table | from person | 必填,表名 |
| columns | select _id,name,age | null 表示 select * |
| selection | where _id=? | null 表示无 where |
| selectionArgs | 替换 ? 的值 | 与 selection 中的 ? 一一对应 |
| groupBy | group by name | null 表示不分组 |
| having | having count>1 | null 表示不过滤分组 |
| orderBy | order by _id desc | null 表示不排序 |
注意selectionArgs的替换机制:selection里写_id=?,selectionArgs传new String[]{"1"},Android 会自动把?替换成带引号的值,这一步天然防了 SQL 注入。这也是 API 方式比手拼字符串更安全的地方。
复习这个练习时,我建议你先把PersonSQLiteOpenHelper建好,表结构保持_id主键自增、name文本、age整数三列,然后按 insert → query → update → delete 的顺序跑一遍,每一步都用 Log 打印影响行数或结果条数。这样你能直观看到insert返回的是新行 id,update/delete返回的是受影响行数,而query返回的是Cursor。把这四个返回值类型记牢,后面排错会快很多。
2. 前置准备:TaoToken 统一 Key 与 Android 调试环境
在动手写 DAO 之前,先把调试通道理顺。你可能会问:本地 SQLite 查询跟 TaoToken 有什么关系?关系在于——复习练习里经常需要「查完本地库,再调一个接口验证数据」,或者用 AI 辅助生成/审查 DAO 代码。这时候如果每个工具都配一套 Key,切换成本很高。TaoToken 的做法是给你一个统一 Key,模型对话、Coding Plan、API 调用共用同一套凭证。
先明确几个地址,后面配置会反复用到:
- 官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- API 基地址:https://taotoken.net/api (这个不加 UTM,直接作为 Base URL 用)
- 模型对话页:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite
- Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite
- 控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
- API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
你需要准备的东西不多:一台能跑 Android Studio 的电脑、一个模拟器或真机、一个已经建好的 Android 项目(minSdk 建议 21 以上)。然后在 TaoToken 控制台创建一个 API Key,复制出来备用。这个 Key 就是后面所有工具共用的那一把。
注意:Key 只显示一次,创建后立刻保存到本地密码管理器或项目的
local.properties(记得加进.gitignore),不要硬编码进BuildConfig后提交到仓库。
Android 侧的环境检查清单:
build.gradle里确认compileSdk和targetSdk一致,避免SQLiteDatabase行为差异。- 模拟器网络正常,能访问外网(用于接口连通性验证)。
- 项目里加一个
NetworkUtils或直接用 OkHttp,方便后面验证 endpoint。 - Logcat 过滤标签设成
PersonDao,方便看增删改查日志。
如果你用 Cline、Claude Code 这类工具辅助写代码,它们的配置也统一指向 TaoToken。以 Cline 的 MCP 配置为例,Base URL 填https://taotoken.net/api,Key 填你创建的那把,Model ID 按文档里支持的模型名填。这样你在编辑器里让 AI 帮你补全PersonDao的query()方法时,走的就是同一条通道,不用再单独配一次。
这里有个复习时容易忽略的点:SQLiteOpenHelper的onCreate只在数据库第一次创建时调用,如果你改了表结构但没改版本号,onUpgrade不会触发,查询就会报「no such column」。所以每次改表结构,记得把版本号 +1,并在onUpgrade里写迁移逻辑或直接 drop 重建(复习阶段 drop 重建可以接受,生产环境不行)。
3. 可复制配置:SQLiteOpenHelper、DAO 与统一 Key 片段
这一节给你可以直接抄的代码和配置。先看PersonSQLiteOpenHelper,这是所有 API 调用的入口:
package com.alexchen.sqliteAPI.db; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; public class PersonSQLiteOpenHelper extends SQLiteOpenHelper { private static final String DB_NAME = "person.db"; private static final int DB_VERSION = 1; public PersonSQLiteOpenHelper(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 varchar(20), age integer)"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("drop table if exists person"); onCreate(db); } }然后是PersonDao里query()的核心模板。注意Cursor用完必须close(),否则连接泄漏,查询次数一多就会报「too many open files」:
public List<Person> queryAll() { List<Person> personList = null; SQLiteDatabase db = mOpenHelper.getReadableDatabase(); if (db.isOpen()) { String[] columns = new String[]{"_id", "name", "age"}; Cursor cursor = db.query("person", columns, null, null, null, null, "_id asc"); if (cursor != null && cursor.getCount() > 0) { personList = new ArrayList<>(); while (cursor.moveToNext()) { int _id = cursor.getInt(cursor.getColumnIndexOrThrow("_id")); String name = cursor.getString(cursor.getColumnIndexOrThrow("name")); int age = cursor.getInt(cursor.getColumnIndexOrThrow("age")); personList.add(new Person(_id, name, age)); } cursor.close(); } db.close(); } return personList; }这里我用getColumnIndexOrThrow替代了原来的下标0/1/2。下标写法在列顺序变化时会静默取错值,getColumnIndexOrThrow列名不存在直接抛异常,复习阶段更容易发现问题。
接下来是统一 Key 的配置片段。如果你用settings.json风格的配置(比如某些编辑器插件),可以这样写:
{ "taotoken": { "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的Key", "modelId": "按文档支持的模型名填写" } }如果你用 TOML 风格(比如 Codex 的auth.json配套配置),对应写法:
[taotoken] base_url = "https://taotoken.net/api" api_key = "sk-你的Key" model_id = "按文档支持的模型名填写"三件套记牢:Base URL + Key + Model ID。缺任何一个,请求都会失败。Base URL 用https://taotoken.net/api,不要自己加/v1或结尾斜杠,具体路径以接入文档为准。
Android 侧如果要调接口验证,用 OkHttp 的最小示例:
OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("https://taotoken.net/api/你的接口路径") .addHeader("Authorization", "Bearer " + BuildConfig.TAOTOKEN_KEY) .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e("PersonDao", "请求失败: " + e.getMessage()); } @Override public void onResponse(Call call, Response response) throws IOException { Log.i("PersonDao", "响应码: " + response.code()); } });BuildConfig.TAOTOKEN_KEY从local.properties读取,在build.gradle里用buildConfigField注入,这样 Key 不会进版本库。
4. 验证请求:一次跑通增删改查并确认返回结果
配置就绪后,按顺序验证。先写一个测试方法,在MainActivity的onCreate里调用,或者用单元测试跑:
PersonDao dao = new PersonDao(this); dao.insert(new Person("张三", 20)); dao.insert(new Person("李四", 22)); List<Person> all = dao.queryAll(); Log.i("PersonDao", "查询到 " + (all == null ? 0 : all.size()) + " 条"); dao.update(1, "张三丰"); Person one = dao.queryItem(1); Log.i("PersonDao", "id=1 的 name=" + one.getName()); dao.delete(2); Log.i("PersonDao", "删除后剩余 " + dao.queryAll().size() + " 条");预期 Logcat 输出:
PersonDao: 插入了1行 PersonDao: 插入了2行 PersonDao: 查询到 2 条 PersonDao: 修改了1行 PersonDao: id=1 的 name=张三丰 PersonDao: 删除了1行 PersonDao: 删除后剩余 1 条如果这几行都对上了,说明本地 SQLite 的 API 增删改查已经跑通。注意insert返回的 id 从 1 开始,update/delete返回的是受影响行数,query返回Cursor或你封装后的List。
接着验证统一 Key 通道的连通性。用 curl 先测,排除 Android 代码干扰:
curl -X POST "https://taotoken.net/api/你的接口路径" \ -H "Authorization: Bearer sk-你的Key" \ -H "Content-Type: application/json" \ -d '{"model":"按文档支持的模型名","messages":[{"role":"user","content":"ping"}]}'返回 200 且 body 里有正常响应,说明 Key 和 Base URL 没问题。如果返回 401,先检查 Key 有没有多余空格、有没有复制完整。如果返回 404,检查路径是不是按接入文档拼的,Base URL 后面不要重复加/api。
Android 侧再跑一次 OkHttp 请求,Logcat 里看到响应码: 200就说明端到端通了。这时候你可以把「本地查询结果」和「接口返回结果」做一次比对,比如本地查到 1 条 person,接口返回的也是同一条,说明数据链路一致。
实测下来,最容易出问题的不是 SQL 本身,而是Cursor没关、数据库没关、Key 带空格这三件事。把这三处检查一遍,基本一次就能跑通。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
复习阶段报错是好事,说明你在真实环境里跑。下面按真实报错对照排查。
401 Unauthorized:Key 无效或没带上。检查Authorization头是不是Bearer sk-xxx格式,中间有一个空格。Android 里如果从local.properties读,注意buildConfigField的引号转义,读出来可能带多余引号。curl 能通、Android 不通,八成是 Key 注入时多了字符。
local proxy failed:本地网络层拦截或代理配置冲突。先确认模拟器网络正常,再检查 Android Studio 的 HTTP Proxy 设置(Settings → Appearance & Behavior → System Settings → HTTP Proxy),设成 No proxy 试一次。如果用了抓包工具,关掉再试。这个报错跟 TaoToken 本身无关,是本地环境问题。
reading choices 相关报错:通常是响应体解析失败,比如你按 OpenAI 格式解析,但实际返回结构不同。先打印原始response.body().string(),看真实 JSON 结构,再调整解析模型。不要凭记忆写解析代码。
OAuth 相关报错:说明你走的鉴权方式和接口要求的不一致。TaoToken 的 API 用 Bearer Key,不需要 OAuth 流程。如果你在某个工具里看到 OAuth 报错,检查是不是选错了鉴权模式,改回 API Key 模式。
no such column: xxx:表结构和你查询的列名不一致。检查onCreate里的建表语句,以及query()里columns数组的列名。改过表结构就升版本号。
Cursor window 相关警告:查询结果集太大,Cursor窗口装不下。加limit或分页,别一次性queryAll几万条。
too many open files:Cursor或SQLiteDatabase没关。确保每个query后cursor.close(),每个getWritableDatabase/getReadableDatabase后db.close()。用 try-finally 包起来最稳。
排查顺序建议:先 curl 确认通道,再 Android 确认代码,最后看 Logcat 完整堆栈。不要一上来就改代码,先定位是通道问题还是代码问题。
6. 把统一 Key 用在长期编码与 Agent 场景
练习九跑通之后,你会发现这套「本地 SQLite API + 统一 Key 通道」的组合可以复用到很多场景。比如你在写PersonDao时让 AI 帮你补全queryItem的边界处理,或者让 Agent 自动生成单元测试,这些都需要一个稳定的模型调用通道。
如果你只是偶尔验证一下模型返回,用模型对话页就够了:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite
如果你要长期做 Android 编码、让 Agent 持续帮你改 DAO、写迁移脚本,那 Coding Plan 更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite
Key 的管理和轮换在控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
新建或吊销 Key 在 API Keys 页:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite
路径和参数细节以接入文档为准:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
回到练习本身,最后给你一个复习技巧:把PersonDao的四个方法各写一个单元测试,用 Robolectric 或 instrumented test 跑,断言insert后queryAll().size()增加、delete后减少。测试通过,说明你对 API 返回值的理解是对的。这比反复手动点按钮可靠得多。