ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android 数据库总结:SQLiteOpenHelper 与 Cursor 实战要点

Android 数据库总结:SQLiteOpenHelper 与 Cursor 实战要点 1. Android 本地持久化为什么总在 SQLiteOpenHelper 和 Cursor 上翻车Android 数据库总结这件事绕不开两个类SQLiteOpenHelper 和 Cursor。前者负责建库、建表、版本升级后者负责把查询结果一行行取出来。听起来简单但真正在项目里写起来问题往往出在细节数据库版本号写死没改、onUpgrade 里直接 DROP 导致用户数据全丢、Cursor 用完忘记 close 造成内存泄漏、getColumnIndex 返回 -1 还硬取值直接崩掉。我见过不少新手项目本地存个游戏分数、缓存个用户配置代码能跑通但一升级版本就出问题或者跑久了内存报警。核心原因就是没把 SQLiteOpenHelper 的生命周期和 Cursor 的资源管理当回事。这篇内容适合谁正在写 Android 本地存储、需要快速自查数据读写链路的开发者对 SQLiteOpenHelper 建库升级流程模糊、Cursor 遍历取数总踩坑的同学以及想把数据库操作封装得更稳、更可维护的人。我会按真实项目里的顺序来先看一个能直接用的 SQLiteOpenHelper 子类再讲版本升级怎么设计才不丢数据然后重点拆 Cursor 的遍历与关闭最后给一套验证请求成功结果的动作和常见报错排查。全程代码可复制参数可对照。需要说明的是本文聚焦 Android 原生 SQLite 的实战要点不涉及任何网络代理或跨境工具。如果你在开发中需要调用大模型能力做代码辅助可以了解下 TaoToken 这类模型接入服务后面会给出合规的配置方式。2. TaoToken 前置准备给 Android 开发加一个模型辅助入口在写数据库代码时很多人会想用 AI 帮忙检查 SQL 语句、生成建表脚本或者排查 Cursor 报错。这时候需要一个稳定的模型接入入口。TaoToken 提供模型对话、Coding Plan、API Keys 和接入文档适合在开发过程中做代码辅助和排障。它的官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 地址不加 UTM 参数。如果你只是想让模型帮你看看 SQLiteOpenHelper 的 onUpgrade 写得对不对可以用模型对话功能deep link 是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。如果你在长期做 Android 编码想有一个稳定的 Agent 辅助可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。需要自己管理 Key 的话控制台在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite API Keys 页面是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。这里要强调一点TaoToken 是模型接入服务不是数据库工具也不替代 Android Studio 或 SQLite。它的作用是当你在写 SQLiteOpenHelper 或调试 Cursor 时能有一个地方快速问、快速验证思路。比如你不确定 onUpgrade 里该用 ALTER TABLE 还是重建表可以把场景描述给模型让它给出几种方案对比。前置准备其实很简单注册后拿到 API Key然后在你的开发辅助工具里配置 Base URL、Key 和 Model ID 三件套。Base URL 用 https://taotoken.net/api Key 从 API Keys 页面获取Model ID 根据你选的模型填。这三件套在 Cline、CC Switch、Codex 的 auth.json 里都是必须的缺一个都连不上。对于 Android 数据库开发来说这个辅助入口最大的价值是在排障阶段。比如你遇到CursorIndexOutOfBoundsException可以把堆栈和你的 query 代码贴给模型让它帮你定位是 moveToFirst 没判断返回值还是 getColumnIndex 拿到了 -1。这比单纯搜报错要快。3. 可复制配置SQLiteOpenHelper 建库升级与 Cursor 遍历片段这一节给可直接复制的代码。先看 SQLiteOpenHelper 子类的完整写法路径放在app/src/main/java/com/example/demo/DbHelper.java。package com.example.demo; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; public class DbHelper extends SQLiteOpenHelper { private static final String DB_NAME mldn.db; private static final int DB_VERSION 2; private static final String TAB_NAME game2048; public DbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { String sql CREATE TABLE TAB_NAME ( id INTEGER PRIMARY KEY AUTOINCREMENT, score INTEGER NOT NULL DEFAULT 0, created_at INTEGER ); db.execSQL(sql); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { db.execSQL(ALTER TABLE TAB_NAME ADD COLUMN created_at INTEGER); } } public long insertScore(int score) { SQLiteDatabase db getWritableDatabase(); ContentValues values new ContentValues(); values.put(score, score); values.put(created_at, System.currentTimeMillis()); long rowId db.insert(TAB_NAME, null, values); db.close(); return rowId; } public int queryMaxScore() { SQLiteDatabase db getReadableDatabase(); Cursor cursor null; int maxScore 0; try { cursor db.query(TAB_NAME, new String[]{MAX(score) AS max_score}, null, null, null, null, null); if (cursor ! null cursor.moveToFirst()) { int index cursor.getColumnIndex(max_score); if (index ! -1) { maxScore cursor.getInt(index); } } } finally { if (cursor ! null) { cursor.close(); } db.close(); } return maxScore; } }这段代码有几个关键点。第一DB_VERSION 从 1 升到 2onUpgrade 里用ALTER TABLE加列而不是 DROP 重建这样用户数据不会丢。第二queryMaxScore 里用 try-finally 保证 Cursor 一定关闭。第三getColumnIndex 之后判断了 -1避免取到不存在的列。如果你用 Cline 或 CC Switch 做开发辅助配置片段如下。Cline 的 MCP 配置里需要 Base URL、Key、Model ID{ mcpServers: { taotoken: { url: https://taotoken.net/api, apiKey: 你的_API_KEY, model: 你的_MODEL_ID } } }Codex 的auth.json路径通常在~/.codex/auth.json内容格式{ base_url: https://taotoken.net/api, api_key: 你的_API_KEY, model: 你的_MODEL_ID }CC Switch 里同样填这三件套Base URL 用https://taotoken.net/apiKey 从 API Keys 页面拿Model ID 按你选的填。这三件套缺一个都会导致连接失败报错通常是 401 或 local proxy failed。回到数据库本身Cursor 遍历取数的标准写法Cursor cursor db.query(TAB_NAME, null, null, null, null, null, id DESC); if (cursor ! null) { while (cursor.moveToNext()) { int idIndex cursor.getColumnIndex(id); int scoreIndex cursor.getColumnIndex(score); if (idIndex ! -1 scoreIndex ! -1) { int id cursor.getInt(idIndex); int score cursor.getInt(scoreIndex); } } cursor.close(); }注意moveToNext()在循环里的用法不要先moveToFirst()再while(moveToNext())那样会漏掉第一行。这是很常见的坑。4. 验证请求与成功结果确认数据读写链路通了写完代码要验证。第一步在 Activity 里创建 DbHelper 并插入数据DbHelper helper new DbHelper(this); long rowId helper.insertScore(2048); int maxScore helper.queryMaxScore();如果 rowId 大于 0说明插入成功。如果 maxScore 等于 2048说明查询链路通了。第二步用 Android Studio 的 Database Inspector 验证。运行 App 后在 Android Studio 菜单栏找到 View Tool Windows App Inspection Database Inspector。选中你的进程展开mldn.db再展开game2048表应该能看到刚插入的行score 字段是 2048created_at 有值。第三步验证升级逻辑。把 DB_VERSION 从 2 改成 3在 onUpgrade 里加一个if (oldVersion 3)分支比如再加一列level INTEGER。重新运行 AppDatabase Inspector 里应该看到新列出现且原有数据还在。如果数据没了说明你写成了 DROP 重建。第四步验证 Cursor 关闭。在 queryMaxScore 里临时把cursor.close()注释掉然后用 Android Studio Profiler 观察内存。反复调用查询方法如果 Cursor 没关闭内存会持续增长。恢复 close 后内存应该稳定。这个动作能帮你确认资源释放是否到位。如果你用 TaoToken 的模型对话辅助验证可以把你的建表语句和 onUpgrade 代码贴进去问它“这个升级逻辑在 oldVersion1 到 newVersion3 时会执行哪些分支”。模型对话入口是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。它能帮你快速检查分支覆盖是否完整。成功结果的标准插入返回 rowId 0查询返回预期值Database Inspector 能看到表和列升级后数据不丢Profiler 里内存不持续增长。这五点都过了数据读写链路就算通了。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节对照真实报错来排查。先说数据库本身的错。CursorIndexOutOfBoundsException通常是moveToFirst()返回 false 时还去getString()。解决方法是先判断moveToFirst()的返回值或者用while(moveToNext())循环。getColumnIndex返回 -1列名拼错了或者查询里没选这列。比如你 query 时传了new String[]{score}却去取id的索引就会返回 -1。解决方法是判断 -1 再取值或者用getColumnIndexOrThrow让它在列不存在时直接抛异常方便定位。SQLiteException: no such tableonCreate 没执行或者表名不一致。检查 DB_VERSION 是否变了导致 onUpgrade 走了错误分支或者 TAB_NAME 常量前后不一致。SQLiteException: table already existsonCreate 里建表前没判断或者 onUpgrade 里重复建表。用CREATE TABLE IF NOT EXISTS可以缓解。再说模型辅助工具连接时的报错。401 UnauthorizedAPI Key 错了或没填。检查 API Keys 页面里的 Key 是否复制完整Base URL 是否是https://taotoken.net/api。三件套里 Key 最容易出错。local proxy failed通常是 Base URL 填错或者本地网络配置有问题。确认 Base URL 没有多余斜杠路径是/api结尾。reading choices相关报错一般是 Model ID 填错或者模型不支持当前请求格式。检查 Model ID 是否和文档里一致接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。OAuth报错如果你用的是需要 OAuth 的工具检查授权是否过期。重新走一遍授权流程或者改用 API Key 方式。对于 Android 数据库开发我建议把模型辅助当成排障工具而不是代码生成器。遇到 Cursor 报错时把堆栈、query 语句、表结构一起贴给模型让它帮你定位。这比只贴一句报错要有效得多。6. 语义一致 CTA按场景选入口如果你是在排障或接入阶段需要 API Key 和接入文档走这两个入口API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。如果你只是想验证模型能不能帮你检查 SQL 或 Cursor 代码用模型对话https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。如果你是长期做 Android 编码或 Agent 辅助需要稳定的 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。最后回到数据库本身。SQLiteOpenHelper 和 Cursor 的坑说到底就是版本管理和资源管理两件事。版本号每次改表结构都要加onUpgrade 里用 ALTER 而不是 DROPCursor 用完一定 closegetColumnIndex 一定判 -1。这四条做到大部分问题都能避开。
返回列表