1. 从一次真实的建库翻车说起
Android 原生 SQLite 开发里,SQLiteOpenHelper、SQLiteDatabase、ContentValues、Cursor这四个类几乎绕不开。它们能做什么?简单说:SQLiteOpenHelper负责建库建表和版本管理,SQLiteDatabase负责执行增删改查,ContentValues负责把键值对安全地塞进表里,Cursor负责把查询结果一行行读出来。适合谁?适合正在做 Android 课程设计、准备面试数据库八股、或者想给本地缓存加一层持久化的同学。
我试过在没配好 AI 辅助环境的情况下,让模型帮我补全onUpgrade逻辑,结果它把oldVersion和newVersion的判断顺序写反了,编译能过,运行直接崩在启动页。问题不在模型本身,而在于我每次对话都要重新贴一遍项目结构、包名、依赖版本,上下文一断,它就开始自由发挥。后来我把 TaoToken 的统一 Key 接进编码工具链,把项目约定写进配置文件,模型每次拿到的都是同一份上下文,建库和 CRUD 的骨架才稳定下来。
这篇就按「建库 → 增删改查 → 验证 → 排障」的顺序,把一套可复制的 SQLite 操作骨架拆开讲,同时给出config.toml和settings.json的接入片段,让你把 AI 辅助编码稳定地用在数据库练习上。
2. TaoToken 前置:统一 Key 与两个配置文件
TaoToken 在这里扮演的角色是「统一入口」:你不需要在多个模型供应商之间来回切换 Key,也不用把不同平台的地址散落在各个工具的配置里。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api (这个不加 UTM)。拿到 Key 之后,控制台在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
为什么要在写 SQLite 之前先配这个?因为数据库代码的「约定」特别多:表名用单数还是复数、主键叫_id还是id、时间字段存INTEGER还是TEXT。这些约定如果每次对话都靠嘴说,模型很容易漂移。把它们固化进配置文件,模型每次读到的都是同一套规则,生成的CREATE TABLE和ContentValues的 key 才不会对不上。
先看config.toml的片段,适合放在项目根目录或者工具约定的配置路径下:
# config.toml —— 项目级 AI 辅助配置 [provider] name = "taotoken" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" # 从环境变量读取,别硬编码 [project] language = "kotlin" min_sdk = 24 package = "com.example.seventhday" [database] helper_class = "MyDbHelper" db_name = "seventh_day.db" db_version = 1 table_cars = "car" column_id = "_id" column_brand = "brand" column_price = "price"再看settings.json,适合那些用 JSON 配置的编辑器插件或 CLI 工具:
{ "taotoken": { "baseUrl": "https://taotoken.net/api", "apiKeyEnv": "TAOTOKEN_API_KEY", "defaultModel": "claude-sonnet", "timeoutMs": 60000 }, "sqliteConvention": { "tablePrefix": "", "primaryKey": "_id", "useContentValues": true, "cursorLoopStyle": "while_moveToNext" } }注意:
api_key_env和apiKeyEnv都指向环境变量,不要把真实 Key 写进版本库。设置环境变量的方式在 macOS/Linux 下是export TAOTOKEN_API_KEY="你的Key",Windows PowerShell 下是$env:TAOTOKEN_API_KEY="你的Key"。
配好之后,你在对话里只需要说「按 config.toml 里的 database 约定,帮我补全 onUpgrade」,模型就能拿到表名、列名、版本号,不用你反复贴。这一步做完,后面的建库代码才有稳定的生成基础。
3. 可复制配置:SQLiteOpenHelper 建库骨架
3.1 继承 SQLiteOpenHelper 并重写生命周期
SQLiteOpenHelper的用法就四步:继承、调父类构造、重写onCreate、重写onUpgrade。onCreate只在数据库第一次创建时调用,onUpgrade只在版本号变大时调用。下面这份 Kotlin 骨架可以直接抄进项目:
package com.example.seventhday import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper class MyDbHelper(context: Context) : SQLiteOpenHelper( context, DB_NAME, null, DB_VERSION ) { companion object { const val DB_NAME = "seventh_day.db" const val DB_VERSION = 1 const val TABLE_CAR = "car" const val COL_ID = "_id" const val COL_BRAND = "brand" const val COL_PRICE = "price" private const val SQL_CREATE_CAR = """ CREATE TABLE $TABLE_CAR ( $COL_ID INTEGER PRIMARY KEY AUTOINCREMENT, $COL_BRAND TEXT NOT NULL, $COL_PRICE REAL NOT NULL DEFAULT 0 ) """ private const val SQL_DROP_CAR = "DROP TABLE IF EXISTS $TABLE_CAR" } override fun onCreate(db: SQLiteDatabase) { db.execSQL(SQL_CREATE_CAR) // 初始数据可选:这里插一条占位记录,方便验证 db.execSQL( "INSERT INTO $TABLE_CAR ($COL_BRAND, $COL_PRICE) VALUES (?, ?)", arrayOf("demo", 0.0) ) } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { // 练习阶段直接重建;生产环境请写增量迁移 if (oldVersion < newVersion) { db.execSQL(SQL_DROP_CAR) onCreate(db) } } }这里有个容易踩的点:onCreate里用execSQL带占位符插入时,第二个参数必须是Array<Any>,Kotlin 里写arrayOf("demo", 0.0)没问题,但如果你写成arrayOf("demo", 0),价格会被当成整数,虽然 SQLite 会做类型亲和转换,但语义上不严谨。
3.2 获取 SQLiteDatabase 并封装 CRUD
拿到SQLiteDatabase的方式是helper.writableDatabase或helper.readableDatabase。写操作走前者,纯查询走后者。下面把增删改查封装成一个 DAO,方便复用:
package com.example.seventhday import android.content.ContentValues import android.content.Context import android.database.Cursor class CarDao(context: Context) { private val helper = MyDbHelper(context) fun insert(brand: String, price: Double): Long { val db = helper.writableDatabase val values = ContentValues().apply { put(MyDbHelper.COL_BRAND, brand) put(MyDbHelper.COL_PRICE, price) } return db.insert(MyDbHelper.TABLE_CAR, null, values) } fun deleteById(id: Long): Int { val db = helper.writableDatabase return db.delete( MyDbHelper.TABLE_CAR, "${MyDbHelper.COL_ID} = ?", arrayOf(id.toString()) ) } fun updatePrice(id: Long, newPrice: Double): Int { val db = helper.writableDatabase val values = ContentValues().apply { put(MyDbHelper.COL_PRICE, newPrice) } return db.update( MyDbHelper.TABLE_CAR, values, "${MyDbHelper.COL_ID} = ?", arrayOf(id.toString()) ) } fun queryAll(): List<Car> { val db = helper.readableDatabase val cursor: Cursor = db.rawQuery( "SELECT * FROM ${MyDbHelper.TABLE_CAR} ORDER BY ${MyDbHelper.COL_ID} ASC", null ) val result = mutableListOf<Car>() while (cursor.moveToNext()) { val id = cursor.getLong(cursor.getColumnIndexOrThrow(MyDbHelper.COL_ID)) val brand = cursor.getString(cursor.getColumnIndexOrThrow(MyDbHelper.COL_BRAND)) val price = cursor.getDouble(cursor.getColumnIndexOrThrow(MyDbHelper.COL_PRICE)) result.add(Car(id, brand, price)) } cursor.close() return result } } data class Car(val id: Long, val brand: String, val price: Double)ContentValues的用法就是new一个对象然后put("列名", 值),注意 key 必须和建表时的列名完全一致,大小写敏感。Cursor的循环固定是while (cursor.moveToNext()),取值用getColumnIndexOrThrow比getColumnIndex更安全,列名写错会直接抛异常而不是返回 -1。
3.3 用 AI 辅助补全时的提示词模板
配好 TaoToken 之后,你可以用这样的提示词让模型补全代码,而不是从零生成:
参考 config.toml 中的 database 约定,为 MyDbHelper 增加一个 onDowngrade 方法, 要求:版本回退时删除 car 表并重建,日志用 android.util.Log 输出,TAG 为 "MyDbHelper"。模型拿到表名、列名、版本号这些固定上下文,生成的代码就能直接编译,不用你手动改列名。
4. 验证请求:插入与查询跑通
配置和代码都就位后,验证动作要足够小,小到一眼能看出对错。推荐在MainActivity的onCreate里加一段临时验证:
val dao = CarDao(this) val newId = dao.insert("byd", 129800.0) android.util.Log.d("SQLiteCheck", "insert id = $newId") val updated = dao.updatePrice(newId, 119800.0) android.util.Log.d("SQLiteCheck", "update rows = $updated") val list = dao.queryAll() list.forEach { android.util.Log.d("SQLiteCheck", "car: id=${it.id}, brand=${it.brand}, price=${it.price}") }跑起来之后,在 Logcat 里过滤SQLiteCheck,你应该看到类似这样的输出:
insert id = 2 update rows = 1 car: id=1, brand=demo, price=0.0 car: id=2, brand=byd, price=119800.0insert返回的是新行的_id,update返回的是受影响行数,queryAll返回的列表里第一条是onCreate时插入的占位记录,第二条是你刚插的。如果insert id返回 -1,说明插入失败,通常是ContentValues的 key 和列名对不上,或者表还没创建。
如果你想让模型帮你解释这段输出,可以直接在对话里贴日志,让它对照config.toml里的列定义分析。模型对话入口在 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite ,适合这种「贴日志问原因」的轻量场景。
5. 本篇常见错排查
5.1 表不存在:no such table: car
这个报错几乎都是onCreate没被调用,或者数据库文件是旧版本残留。onCreate只在数据库文件第一次创建时执行,如果你之前跑过一次、数据库已经存在,改完建表语句再跑是不会重新执行的。解决办法有两个:一是卸载应用重装,二是把DB_VERSION加一,让onUpgrade触发重建。练习阶段推荐后者,顺便验证onUpgrade逻辑。
5.2 列名对不上:no such column: brand
ContentValues.put的 key、Cursor.getColumnIndexOrThrow的参数、建表语句里的列名,这三处必须完全一致。常见错误是建表用car_brand,put的时候写成brand。把列名抽成companion object里的常量,三处引用同一个常量,就能从根上避免。这也是为什么第 2 节的config.toml里要把列名写进去——让模型也引用同一份定义。
5.3 Cursor 忘记 close 导致泄漏
Cursor用完必须close(),否则会泄漏游标资源,查询多了之后应用会变卡甚至崩。上面的queryAll里用了cursor.close(),但如果你在while循环中间return,close就执行不到了。更稳的写法是用use扩展:
helper.readableDatabase.rawQuery(sql, null).use { cursor -> while (cursor.moveToNext()) { // 读取逻辑 } }use会在代码块结束时自动关闭,不管有没有异常。
5.4 onUpgrade 里旧版本判断写反
onUpgrade(db, oldVersion, newVersion)里,oldVersion是设备上已有的版本,newVersion是代码里声明的版本。判断条件应该是if (oldVersion < newVersion),而不是反过来。写反了会导致升级时什么都不做,或者降级时反而重建表。这个错误模型很容易犯,因为它在生成代码时倾向于「对称写法」。把版本判断逻辑写进config.toml的注释里,让模型每次都能看到正确方向。
5.5 主键自增但插入时手动指定 _id
INTEGER PRIMARY KEY AUTOINCREMENT的列,插入时不要手动put("_id", 值),否则自增序列会被打乱。如果你确实需要指定 id,用insertWithOnConflict并处理冲突策略。练习阶段直接让数据库自己分配 id 最省事。
6. 把 AI 辅助稳定接进数据库练习
数据库操作的练习有个特点:代码量不大,但约定特别密。表名、列名、版本号、主键策略、Cursor 循环方式,任何一处不一致都会报错,而报错信息往往只告诉你「列不存在」,不告诉你哪一处写错了。把 TaoToken 的统一 Key 接进工具链,再把项目约定固化进config.toml和settings.json,模型每次拿到的上下文就是同一份,生成的建表语句和 CRUD 代码才能对得上。
如果你后面要长期做 Android 编码练习,或者想让 Agent 帮你批量生成 DAO,可以看看 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 ,里面有各语言 SDK 的调用示例。Claude Code 相关的配置参考 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude-code-anthropic&utm_campaign=rewrite 。
最后留一个实用技巧:每次改完建表语句,先把DB_VERSION加一,再跑验证代码,看 Logcat 里的insert id和queryAll输出。如果输出里出现了旧数据和新数据混在一起,说明onUpgrade的删除逻辑没生效,回去检查DROP TABLE的语句有没有拼错表名。这个检查动作花不了两分钟,但能帮你省掉半小时的「为什么列不存在」排查。