news 2026/9/27 20:57:00

Android中SQLite使用:从建库到TaoToken配置的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android中SQLite使用:从建库到TaoToken配置的完整实践

1. Android 本地存储为什么绕不开 SQLite

做 Android 应用,只要涉及「数据要留在手机上、下次打开还在」,SQLite 基本是默认答案。它是 Android 系统内置的轻量级关系型数据库,不需要单独装服务,一个 App 一个 db 文件,支持事务、索引、SQL 语法,适合存用户配置、聊天记录、离线缓存、待办清单这类结构化数据。适合谁?刚接触 Android 存储的新手、想把手写 SQL 封装成可维护代码的开发者,以及准备在本地数据之上再接入 AI 能力的同学。

我见过太多项目把 SQL 语句散落在 Activity 里,改一个字段要全局搜字符串,升级表结构时直接崩在用户手机上。这篇就按「建库 → 建表 → 增删改查 → 事务 → 版本升级」的主线走一遍,交付一份可以直接复制的 SQLiteOpenHelper 骨架和 DBManager 封装。后半段再补上很多同学会忽略的一环:本地数据写好了,如果还想调用大模型做摘要、分类、问答,Key 和通道怎么统一配置。这里用 TaoToken 的 API 通道做示例,把 config.toml 片段和验证命令一起给出来,让数据持久化和 AI 调用在同一个项目里跑通。

核心检索词先摆清楚:Android SQLite 使用,本质是「用 SQLiteOpenHelper 管理数据库生命周期 + 用 SQLiteDatabase 执行增删改查」。能做什么?本地持久化、事务批量写入、Cursor 查询、版本迁移。适合谁?所有需要离线数据的 Android 项目。

2. 建库建表:SQLiteOpenHelper 骨架

SQLiteOpenHelper 是官方给的抽象类,负责两件事:数据库第一次创建时调 onCreate,版本号变大时调 onUpgrade。你只要继承它,把建表 SQL 写进去,剩下的打开、加锁、版本比对它都帮你做了。

先定义数据模型。一个 Person 类,字段和表列一一对应:

package com.scott.db; public class Person { public int _id; public String name; public int age; public String info; public Person() {} public Person(String name, int age, String info) { this.name = name; this.age = age; this.info = info; } }

接着是 DBHelper。注意版本号常量,以后加字段就靠它触发 onUpgrade:

package com.scott.db; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME = "person.db"; private static final int DB_VERSION = 1; public static final String TABLE = "person"; public static final String COL_ID = "_id"; public static final String COL_NAME = "name"; public static final String COL_AGE = "age"; public static final String COL_INFO = "info"; public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { String sql = "CREATE TABLE " + TABLE + " (" + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + COL_NAME + " TEXT NOT NULL, " + COL_AGE + " INTEGER, " + COL_INFO + " TEXT)"; db.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 简单粗暴的迁移:先删后建,生产环境请按版本逐级 ALTER db.execSQL("DROP TABLE IF EXISTS " + TABLE); onCreate(db); } }

这里有个细节值得说清楚:主键列名用_id不是随便起的。SimpleCursorAdapter 要求结果集里必须有_id列,否则直接抛异常。这是 SQLite 在 Android 里的约定,建表时就按规范来,后面省很多事。

3. DBManager 封装:增删改查与事务

有了 Helper,再包一层 DBManager,把业务方法集中管理。构造方法里拿到 SQLiteDatabase 实例,整个应用共用一份。

package com.scott.db; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import java.util.ArrayList; import java.util.List; public class DBManager { private DBHelper helper; private SQLiteDatabase db; public DBManager(Context context) { helper = new DBHelper(context); // 放在 Activity 的 onCreate 里实例化,确保 context 已初始化 db = helper.getWritableDatabase(); } public void add(List<Person> persons) { db.beginTransaction(); try { for (Person p : persons) { db.execSQL("INSERT INTO " + DBHelper.TABLE + " VALUES(null, ?, ?, ?)", new Object[]{p.name, p.age, p.info}); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } } public void updateAge(Person person) { ContentValues cv = new ContentValues(); cv.put(DBHelper.COL_AGE, person.age); db.update(DBHelper.TABLE, cv, DBHelper.COL_NAME + "=?", new String[]{person.name}); } public void deleteOldPerson(Person person) { db.delete(DBHelper.TABLE, DBHelper.COL_AGE + ">=?", new String[]{String.valueOf(person.age)}); } public List<Person> query() { ArrayList<Person> persons = new ArrayList<>(); Cursor c = queryTheCursor(); while (c.moveToNext()) { Person p = new Person(); p._id = c.getInt(c.getColumnIndex(DBHelper.COL_ID)); p.name = c.getString(c.getColumnIndex(DBHelper.COL_NAME)); p.age = c.getInt(c.getColumnIndex(DBHelper.COL_AGE)); p.info = c.getString(c.getColumnIndex(DBHelper.COL_INFO)); persons.add(p); } c.close(); return persons; } public Cursor queryTheCursor() { return db.rawQuery("SELECT * FROM " + DBHelper.TABLE, null); } public void closeDB() { db.close(); } }

几个关键点展开说。批量插入用事务,beginTransaction到setTransactionSuccessful之间是原子操作,中途出错会整体回滚,比一条条 insert 快一个数量级。查询完记得c.close(),Cursor 不关会泄漏。closeDB在应用最后一个 Activity 销毁时调用,这一步最容易被忘,但数据库连接不释放,长期运行会出问题。

关于getWritableDatabase()和getReadableDatabase()的选择:看源码会发现,getReadableDatabase()内部先尝试调getWritableDatabase(),只有在磁盘满等异常时才降级为只读打开。所以正常情况下两者返回的是同一个实例。直接用getWritableDatabase()拿全局实例是可行的;如果你确实担心只读降级场景,可以 try-catch 后再降级获取。

4. TaoToken 前置:统一 Key 与 API 通道

本地数据跑通后,很多场景会想接一层 AI 能力,比如把 SQLite 里的记录做摘要、分类、生成周报。这时候如果每个模型都单独配 Key、单独改 base_url,项目会变得很难维护。TaoToken 的思路是提供一个统一的 API 通道,把模型调用收敛到一个入口,Key 和地址集中管理。

先到官网注册并进入控制台创建 API Key:

  • 官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
  • 控制台(创建 Key):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

API 基础地址统一用:https://taotoken.net/api

拿到 Key 之后,不要硬编码在 Java 里。推荐放在项目根目录的 config.toml,或者用 local.properties + BuildConfig 注入。config.toml 片段示例:

# config.toml [taotoken] base_url = "https://taotoken.net/api" api_key = "sk-你的Key" model = "claude-sonnet-4-20250514" timeout_seconds = 60

如果你用的是支持 TOML 配置的客户端或脚本,直接读这个文件即可。Android 项目里更常见的做法是把 Key 写进 local.properties,再在 build.gradle 里读出来塞进 BuildConfig,避免提交到仓库。

注意:Key 属于敏感凭证,不要写进版本控制,也不要打包进 APK 的明文资源里。生产环境建议走服务端转发。

5. 可复制配置与验证请求

配置写好后,第一步是验证通道是否通。用 curl 发一个最小请求,确认 Key 和地址都对:

curl -X POST "https://taotoken.net/api/v1/messages" \ -H "Content-Type: application/json" \ -H "x-api-key: sk-你的Key" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-sonnet-4-20250514", "max_tokens": 128, "messages": [ {"role": "user", "content": "用一句话说明SQLite事务的作用"} ] }'

返回体里能看到content数组,说明通道正常。如果返回 401,检查 Key 是否复制完整;返回 404,检查 base_url 有没有多写或少写/v1。

在 Android 侧,把 SQLite 查询结果拼成 prompt 再发请求,逻辑上就是「读本地 → 组消息 → 调 API → 写回本地」。下面是一个用 OkHttp 发请求的骨架:

// 伪代码骨架,重点看参数组织 String json = "{" + "\"model\":\"" + BuildConfig.TAO_MODEL + "\"," + "\"max_tokens\":256," + "\"messages\":[{\"role\":\"user\",\"content\":\"" + prompt + "\"}]" + "}"; Request request = new Request.Builder() .url(BuildConfig.TAO_BASE_URL + "/v1/messages") .addHeader("x-api-key", BuildConfig.TAO_API_KEY) .addHeader("anthropic-version", "2023-06-01") .addHeader("Content-Type", "application/json") .post(RequestBody.create(json, MediaType.parse("application/json"))) .build();

prompt 里可以把 Person 表的记录序列化进去,比如「以下是用户最近的记录,请归纳成三条要点」。返回结果再通过 DBManager 的 update 方法写回某一列,本地数据和 AI 结果就串起来了。

想先在网页上试模型效果,可以直接用模型对话页面:

  • 模型对话:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite

如果你是要长期在 IDE 里做编码、跑 Agent 任务,建议看 Coding Plan,额度模型更适合持续调用:

  • 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

6. 本篇常见错排查

报错一:no such table: person多数是 DBHelper 的 onCreate 没执行,或者数据库版本号没变但表结构改了。检查 DB_VERSION 是否递增,onUpgrade 里有没有正确处理。另一个常见原因是 Context 传了 null,导致 openOrCreateDatabase 失败。

报错二:CursorIndexOutOfBoundsExceptiongetColumnIndex返回 -1 时直接取值就会越界。用getColumnIndexOrThrow替代,列名写错时能立刻定位。列名建议统一用常量,别手写字符串。

报错三:SimpleCursorAdapter 不显示数据九成是结果集里没有_id列。解决办法有三种:建表时主键就叫_id;查询时用别名SELECT id AS _id FROM person;或者在 CursorWrapper 里重写getColumnIndexOrThrow做映射。

报错四:事务没生效或数据没写进去检查setTransactionSuccessful()是否在endTransaction()之前调用。只有标记成功,endTransaction 才会提交,否则回滚。另外事务里不要做网络请求,会长时间持锁。

报错五:API 返回 401 / 403Key 失效、复制时带了空格、或者请求头字段名写错。Anthropic 风格用x-api-key,OpenAI 风格用Authorization: Bearer,别混用。base_url 确认是https://taotoken.net/api,不要自己拼错路径。

报错六:Cursor 泄漏导致 ANR查询完必须c.close(),或者用 try-with-resources。Activity 里可以用startManagingCursor交给系统按生命周期管理,但新版本更推荐手动关闭或用 Room。

7. 继续深入的方向

SQLite 这套骨架跑通后,下一步可以往三个方向走。一是把 DBManager 换成 Room,用注解生成代码,编译期校验 SQL,减少手写错误。二是加索引,对经常查询的列建 index,数据量上万后差别很明显。三是把本地数据和 AI 调用做成流水线,比如定时把新增记录批量送去摘要,结果写回新表。

如果你在接入 AI 通道时想省去逐个模型配 Key 的麻烦,可以按上面的 config.toml 把 base_url 和 Key 统一到 TaoToken,再配合 API Keys 页面管理多套凭证。编码场景长期用的话,Coding Plan 的额度模型会比按次调用更划算。先把 SQLite 的增删改查和事务写扎实,再叠 AI 能力,整个链路会稳很多。

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

Grix 移动端与桌面端 DeepSeek Harness 配置 TaoToken 统一 Key 通道

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

作者头像 李华
网站建设 2026/9/27 20:52:30

DeepSeek量化回测全流程:因子评估、LoRA微调与部署优化

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

作者头像 李华
网站建设 2026/9/27 20:51:59

小米职级薪资待遇一览:从P4到P9总包与晋升路径全解析

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

作者头像 李华
网站建设 2026/9/27 20:51:56

网页圆形透明滤镜:用 CSS mask 与 backdrop-filter 实现头像镂空效果

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

作者头像 李华
网站建设 2026/9/27 20:49:15

CODESYS安装配置全攻略:从版本选型到PLC连接与常见报错排查

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

作者头像 李华