ARTICLE DETAIL

资讯详情

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

Android中SQLite应用详解(2):TaoToken统一Key通道下的数据读写与事务验证

Android中SQLite应用详解(2):TaoToken统一Key通道下的数据读写与事务验证 1. Android SQLite 数据落库场景从接口返回到本地表Android 本地 SQLite 是每个 App 都绕不开的存储底座它是什么简单说就是系统内置的轻量级关系型数据库不需要额外安装服务一个.db文件就能承载增删改查和事务。它能做什么缓存接口返回、离线展示列表、记录用户操作日志、保存草稿。适合谁适合所有需要在设备端持久化结构化数据的 Android 开发者尤其是做列表页、离线优先、弱网兜底的同学。这一篇聚焦一个真实场景App 从远端接口拿到一批 Person 数据先写入本地 SQLite再通过事务保证批量插入的原子性最后用 adb 和日志验证数据一致性。接口调用侧我们统一走 TaoToken 的 Key 通道把模型对话或业务接口的返回结果落到本地表里这样既演示了 SQLite 的读写也把「接口数据 → 本地落库」这条链路走通。很多同学写 SQLite 只停留在insert单条遇到批量写入失败一半就懵了。事务就是解决这个问题的要么全部成功要么全部回滚。我会给出可复制的SQLiteOpenHelper建库建表配置、事务提交与回滚代码片段以及用adb shell和Logcat验证数据的具体动作。你跟着做一遍就能在自己的工程里复现一次完整的本地存储验证。先明确几个核心检索词方便你对照Android SQLite 增删改查、SQLiteOpenHelper 建库建表、SQLite 事务提交与回滚、adb 验证 SQLite 数据、Android 本地存储数据一致性。这些词贯穿全文也是你搜索排障时最常用的入口。我试过在弱网环境下批量写 500 条记录不用事务时中途失败会留下脏数据用事务后要么全进要么全不进排查成本直接降下来。下面从建库开始一步步把这条链路搭起来。2. TaoToken 统一 Key 通道前置准备在把接口数据落到 SQLite 之前先要把接口调用侧准备好。TaoToken 在这里扮演的是统一 Key 通道的角色你不需要在 App 里散落多个厂商的 Key而是通过一个统一的入口管理模型对话、coding-plan、console、api-keys 等能力。对于 Android 工程来说接口返回的 JSON 就是我们要落库的数据源。先访问官网了解整体能力https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。API 入口是 https://taotoken.net/api 注意这个地址不加 UTM 参数直接用于代码里的 Base URL。你需要准备三件套Base URL、API Key、Model ID。这三者在任何接入场景里都是绑定的缺一不可。Base URL 填https://taotoken.net/apiAPI Key 在控制台生成Model ID 根据你用的模型填写。生成 Key 的入口在这里https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。如果你只是想先验证模型返回可以用模型对话页面快速试一条请求https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。长期做编码或 Agent 任务建议看 coding-planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。控制台在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。为什么 Android 工程要关心这个因为你的数据来源是接口接口的稳定性、Key 的统一管理直接影响落库的成功率。把 Key 通道理顺后面 SQLite 的事务才有意义——数据源都拿不到事务回滚也无从谈起。在 Android 侧我建议把接口调用封装成一个 Repository返回ListPerson然后交给 DBManager 落库。这样职责清晰网络层只管拿数据存储层只管写数据。下面先给建库建表的配置。3. 可复制配置SQLiteOpenHelper 建库建表与事务代码这一节是全文的技术核心给出可直接复制的配置和代码。先看SQLiteOpenHelper的建库建表数据库名test.db版本号1表名person字段_id、name、age、info。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 DATABASE_NAME test.db; private static final int DATABASE_VERSION 1; public static final String TABLE_NAME person; private static final String CREATE_TABLE CREATE TABLE TABLE_NAME ( _id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER, info TEXT); public DBHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_TABLE); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(DROP TABLE IF EXISTS TABLE_NAME); onCreate(db); } }建表语句里_id用INTEGER PRIMARY KEY AUTOINCREMENT这是SimpleCursorAdapter能正常工作的前提它要求结果集里必须有_id列。name设为NOT NULL避免插入空名导致列表展示异常。接下来是 DBManager包含增删改查和事务。批量插入用beginTransactionsetTransactionSuccessfulendTransaction三件套这是事务的标准写法。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); db helper.getWritableDatabase(); } public void add(ListPerson persons) { db.beginTransaction(); try { for (Person person : persons) { db.execSQL(INSERT INTO person VALUES(null, ?, ?, ?), new Object[]{person.name, person.age, person.info}); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } } public void updateAge(Person person) { ContentValues cv new ContentValues(); cv.put(age, person.age); db.update(person, cv, name ?, new String[]{person.name}); } public void deleteOldPerson(Person person) { db.delete(person, age ?, new String[]{String.valueOf(person.age)}); } public ListPerson query() { ArrayListPerson persons new ArrayList(); Cursor c queryTheCursor(); while (c.moveToNext()) { Person person new Person(); person._id c.getInt(c.getColumnIndex(_id)); person.name c.getString(c.getColumnIndex(name)); person.age c.getInt(c.getColumnIndex(age)); person.info c.getString(c.getColumnIndex(info)); persons.add(person); } c.close(); return persons; } public Cursor queryTheCursor() { return db.rawQuery(SELECT * FROM person, null); } public void closeDB() { db.close(); } }这里有个关键点getWritableDatabase()和getReadableDatabase()的选择。getReadableDatabase()内部会先尝试调用getWritableDatabase()只有在磁盘满等异常时才降级为只读。所以正常情况下两者返回的是同一个实例。我在 DBManager 构造方法里直接用getWritableDatabase()因为整个应用需要写能力避免后续写操作时再切换。如果你担心极端情况可以先用getWritableDatabase()捕获异常后再用getReadableDatabase()兜底但此时实例只能读不能写。对于大多数 App直接用可写实例就够了。事务的写法要特别注意setTransactionSuccessful()必须在try块内、endTransaction()之前调用否则事务会被回滚。endTransaction()放在finally里保证无论是否异常都会结束事务。这是最容易踩的坑之一。4. 验证请求与成功结果adb 与日志确认数据一致性代码写完了怎么确认数据真的落库了两条路adb 直接查库Logcat 看日志。先看 adb 的方式。连接设备后进入应用数据库目录。注意不同设备路径可能不同用run-as以应用身份访问adb shell run-as com.scott.db cd databases ls你会看到test.db文件。用 sqlite3 打开它部分设备自带 sqlite3没有的话可以把 db 文件 pull 出来用本地工具看sqlite3 test.db .tables SELECT * FROM person;正常插入 5 条后你应该看到类似结果1|Ella|22|lively girl 2|Jenny|22|beautiful girl 3|Jessica|23|sexy girl 4|Kelly|23|hot baby 5|Jane|25|a pretty woman如果SELECT返回空说明插入没成功回到事务代码检查setTransactionSuccessful()是否被调用。再看 Logcat 验证。在 DBManager 的 add 方法里加一行日志打印插入条数public void add(ListPerson persons) { db.beginTransaction(); int count 0; try { for (Person person : persons) { db.execSQL(INSERT INTO person VALUES(null, ?, ?, ?), new Object[]{person.name, person.age, person.info}); count; } db.setTransactionSuccessful(); } finally { db.endTransaction(); android.util.Log.d(DBManager, inserted rows count); } }用 adb 过滤日志adb logcat -s DBManager成功时输出inserted rows 5。如果事务中途抛异常count会小于 5且数据库里一条都不会有这就是事务的原子性。验证回滚故意在循环里插入一条非法数据比如name为 null触发NOT NULL约束异常。此时setTransactionSuccessful()不会执行endTransaction()回滚数据库保持原状。你可以先清空表再测sqlite3 test.db DELETE FROM person;然后跑一次带异常的插入再SELECT确认表为空。这一步能让你直观看到事务回滚的效果。接口侧的数据落库演示假设你从 TaoToken 的模型对话接口拿到一段 JSON解析成ListPerson再调用mgr.add(persons)。接口返回的字段名和表字段对应上落库就完成了。验证时先确认接口返回非空再确认数据库有数据两段日志对照看。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth排障部分按真实报错来。第一个是 401通常出现在接口调用侧说明 API Key 无效或没带上。检查你的请求头里Authorization是否正确Key 是否在控制台生成且未过期。生成入口https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。401 和 SQLite 本身无关但它会导致数据源为空落库自然没数据所以先排接口再排存储。第二个是local proxy failed这类报错多出现在本地网络配置异常时。检查你的 Base URL 是否写成https://taotoken.net/api不要多加路径或参数。如果用了本地代理工具确认其状态正常。这个报错会直接阻断接口请求导致ListPerson为空add方法插入 0 条日志显示inserted rows 0。第三个是reading choices相关报错通常出现在解析接口返回时字段缺失或格式不符。比如你期望choices[0].message.content但返回结构不同解析抛异常。此时落库前就崩了SQLite 根本没被调用。解决办法是先打印原始返回体确认字段路径再写解析逻辑。第四个是 OAuth 相关报错出现在需要鉴权的接入场景。确认你的鉴权流程是否走完token 是否刷新。OAuth 失败同样导致接口拿不到数据落库链路断在第一步。除了接口侧SQLite 侧也有几个高频错。no such table: person说明建表没执行检查onCreate是否被调用、数据库版本号是否变化。UNIQUE constraint failed说明主键冲突检查插入时_id是否传了 null应该传 null 让自增生效。CursorWindowAllocationException说明一次查询数据量太大分批查或加 limit。对照排查顺序先确认接口返回401 / local proxy failed / reading choices / OAuth再确认解析结果非空最后确认 SQLite 写入。三段日志分别打问题定位会快很多。6. 语义一致 CTA把这条链路用到你的工程里到这里建库建表、事务读写、adb 验证、报错排查都走了一遍。你可以把这套代码直接搬进自己的工程DBHelper 负责建表DBManager 负责增删改查和事务Repository 负责接口调用三者职责分明。接口侧统一走 TaoToken 的 Key 通道Base URL 用https://taotoken.net/apiKey 在控制台管理Model ID 按需填写。需要快速验证模型返回用模型对话https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。长期做编码或 Agent 任务看 coding-planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。接入细节查文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。最后留一个实用技巧在onDestroy里调用mgr.closeDB()释放数据库资源。这个步骤容易被忘记但长期运行的应用不关库会累积文件句柄。另外批量插入尽量放在子线程避免主线程阻塞导致 ANR。事务本身不慢慢的是在主线程做大量 IO。把接口数据落到本地 SQLite再用事务保证一致性这条链路在离线优先的 App 里非常常见。你按上面的步骤做一遍遇到报错对照第 5 节排查基本能覆盖大部分场景。
返回列表