ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Android SQLite 报错 column ‘_id‘ does not exist:Cursor 与 Adapter 的排查与修复

2026/10/3 16:24:10 拓冰建站 浏览量
Android SQLite 报错 column ‘_id‘ does not exist:Cursor 与 Adapter 的排查与修复 1. 从一次真机崩溃说起column _id does not exist 到底卡在哪column _id does not exist这个报错几乎每个写过 Android SQLite ListView 组合的人都会撞上一次。它的完整堆栈通常长这样java.lang.IllegalArgumentException: column _id does not exist at android.database.AbstractCursor.getColumnIndexOrThrow(AbstractCursor.java:333) at android.widget.CursorAdapter.init(CursorAdapter.java:174) at android.widget.CursorAdapter.init(CursorAdapter.java:127) at android.widget.SimpleCursorAdapter.init(SimpleCursorAdapter.java:104)注意最后三行问题不在你的query()调用而在SimpleCursorAdapter构造的那一刻。CursorAdapter的基类在初始化时会强制调用getColumnIndexOrThrow(_id)也就是说只要你想把 Cursor 交给任何 CursorAdapter 家族SimpleCursorAdapter、CursorAdapter、CursorTreeAdapter去绑定结果集里就必须存在一个叫_id的列名字一字不差大小写敏感。这就是为什么很多人明明表里有主键、有自增、有id程序还是崩。因为 SQLite 允许你随便命名主键但 Android 的 Adapter 层不认id只认_id。它俩是两个世界的东西一个是数据库设计自由一个是框架硬性约定。这个报错适合谁看如果你正在用SimpleCursorAdapter往ListView塞数据、用CursorLoader配合RecyclerView、或者在做老项目维护时突然遇到列表空白加崩溃那这篇就是给你写的。我会从建表 SQL、查询投影、Cursor 列名校验、Adapter 改造四个层次逐层拆每一层都给可复制的代码最后用adb走一遍复现和验证。先说结论方便你带着方向读修复的核心只有一句话——让 Cursor 的结果集里出现一个名为_id的列。实现方式有两种改表结构或者在查询时用AS映射。下面展开。2. 前置准备用 TaoToken 打通模型辅助排查链路排查这类报错最有效的方式不是盲搜而是把完整堆栈、建表语句、查询代码一起丢给一个能读懂上下文的模型让它帮你定位是哪一层出的问题。我平时用 TaoToken 来做这件事它把常见大模型的调用统一到一个入口省得在多个平台之间来回切。TaoToken 是什么一个聚合多家大模型能力的 API 平台提供统一的 Base URL 和 Key兼容 OpenAI 风格的接口协议。能做什么你可以用它跑对话排查代码、做代码补全、接进 IDE 插件做实时提示。适合谁正在写 Android、需要频繁查报错和生成样板代码的开发者尤其是想把模型接进自己工具链的人。接入前你需要准备三样东西这也是后面所有配置的基础我把它叫「三件套」配置项值说明Base URLhttps://taotoken.net/api所有请求的统一入口注意不要带多余路径API Key在控制台生成形如sk-xxxx只显示一次记得存好Model ID按需选择例如对话类、代码类模型各有对应 ID获取 Key 的路径打开 https://taotoken.net/api-keys 登录后在控制台创建。创建完立刻复制页面刷新后就看不到了。如果你只是想先验证模型能不能正常回话可以直接去 https://taotoken.net/models 用网页版对话试一句确认账号和额度没问题再回来配代码。这里有个我踩过的坑很多人把 Base URL 写成https://taotoken.net/api/v1或者带上一堆后缀结果请求 404。正确做法是 Base URL 只写到/api具体路径由你用的 SDK 或工具自己拼。比如 OpenAI 官方 SDK 会自动在末尾加/chat/completions你手动再加就重复了。对于长期要写 Android、经常和 Cursor、Adapter、SQLite 打交道的人如果打算把模型接进日常编码流程比如让它在 IDE 里补全 SQL、解释堆栈可以了解一下 Coding Planhttps://taotoken.net/coding-plan 。它面向的是持续编码和 Agent 场景比单次对话更适合高频使用。如果只是偶尔查个报错用模型对话页就够了。配好之后你就可以把下面这段真实堆栈丢进去问java.lang.IllegalArgumentException: column _id does not exist at android.widget.CursorAdapter.init(CursorAdapter.java:174) 我的建表语句是 CREATE TABLE jokes (id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT); 查询是 db.query(jokes, new String[]{id,content}, ...); 为什么报这个错模型会直接告诉你_id和id的区别比你自己翻半天文档快得多。这一步不是必须的但它能帮你建立「先定位层次、再动手改」的习惯而不是看到报错就乱改代码。3. 可复制配置建表 SQL、查询投影与 Adapter 改造片段这一节是全文的核心我把修复拆成三条路径你可以按自己的项目情况选。每条都给完整可复制的代码。3.1 路径一建表时直接使用 _id 作为主键最干净的做法是从一开始就把主键命名为_id。SQLite 完全接受这个名字Android 框架也认。CREATE TABLE jokes ( _id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, author TEXT, created_at INTEGER );注意AUTOINCREMENT必须配合INTEGER PRIMARY KEY使用写成TEXT PRIMARY KEY或者INT PRIMARY KEY都不行自增会失效。如果你用的是Room对应写法是Entity(tableName jokes) data class Joke( PrimaryKey(autoGenerate true) ColumnInfo(name _id) val id: Long 0, val content: String, val author: String? )ColumnInfo(name _id)这一行是关键它让 Room 生成的底层表列名就是_id而不是默认的字段名id。很多人用 Room 时忘了加这个注解结果底层表还是id一接 CursorAdapter 就崩。3.2 路径二不改表结构查询时用 AS 映射如果你的表已经上线、数据不能动或者你根本不想为了 Adapter 去改表那就在查询投影里做映射。这是最常用的补救方案。SQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.query( jokes, // 表名 new String[]{id AS _id, content, author}, // 投影注意 AS null, null, null, null, created_at DESC );关键就是id AS _id。SQLite 支持在 SELECT 列表里给列起别名AS之后的名字会成为 Cursor 里的列名。这样cursor.getColumnIndex(_id)就能拿到值CursorAdapter 也不会再抛异常。对应的 Adapter 绑定代码SimpleCursorAdapter adapter new SimpleCursorAdapter( this, R.layout.item_joke, cursor, new String[]{_id, content, author}, // fromCursor 里的列名 new int[]{R.id.tv_id, R.id.tv_content, R.id.tv_author}, // to布局里的控件 0 ); listView.setAdapter(adapter);from数组里的名字必须和 Cursor 结果集的列名完全一致。如果你投影写的是id AS _id这里就写_id如果你投影写的是id这里写_id就会再次报错。这是第二个高频坑。3.3 路径三CursorLoader 场景下的投影用CursorLoader时投影是在onCreateLoader里指定的逻辑一样Override public LoaderCursor onCreateLoader(int id, Bundle args) { return new CursorLoader( this, JokeContract.CONTENT_URI, new String[]{id AS _id, content, author}, // 投影 null, null, created_at DESC ); }如果你用的是ContentProvider还要注意query()方法里对投影的处理。有些 Provider 实现会校验投影列是否存在于表中遇到id AS _id这种带别名的字符串会解析失败。这种情况下要么在 Provider 里放行别名要么干脆改表结构用_id。3.4 一个容易忽略的点DISTINCT 和 JOIN 场景当你用DISTINCT或者多表JOIN时AS _id依然有效但要确保别名唯一SELECT j.id AS _id, j.content, a.name AS author_name FROM jokes j LEFT JOIN authors a ON j.author_id a.id如果两张表都有id不加别名直接SELECT *Cursor 里会出现两个id列getColumnIndex返回第一个行为不可预期。所以投影永远显式写列名别偷懒用*。4. 验证请求与成功结果adb 复现 修复确认改完代码不能只看编译通过得在真机或模拟器上跑一遍确认 Cursor 里真的有_id。下面是我常用的验证流程。4.1 用 adb 复现原始报错先确认你能稳定复现这样才知道修复有没有生效。启动应用触发列表页然后抓日志adb logcat -c adb logcat | grep -i column _id如果看到IllegalArgumentException: column _id does not exist说明复现成功。记下这个状态改完代码后再跑一次报错消失才算修好。4.2 在代码里打印 Cursor 列名最直接的验证方式是在绑定 Adapter 之前把 Cursor 的所有列名打出来Cursor cursor db.query(jokes, new String[]{id AS _id, content, author}, null, null, null, null, null); String[] names cursor.getColumnNames(); for (String name : names) { Log.d(CursorCheck, column: name); }期望输出column: _id column: content column: author如果第一行是id而不是_id说明你的AS没生效检查 SQL 字符串有没有拼错或者是不是被某个中间层改写了投影。4.3 用 getColumnIndexOrThrow 主动校验在交给 Adapter 之前主动校验一次把问题暴露在离现场更近的地方int idIndex cursor.getColumnIndexOrThrow(_id); Log.d(CursorCheck, _id index idIndex);如果这里就抛异常说明投影确实没带上_id不用等到 Adapter 构造。这个习惯能帮你把报错定位到具体哪一行代码而不是一堆框架堆栈。4.4 成功结果长什么样修复后列表正常显示数据adb logcat里不再有column _id does not exist点击列表项能正确拿到_id对应的行 IDlistView.setOnItemClickListener((parent, view, position, id) - { // id 就是 Cursor 里 _id 列的值 Log.d(ItemClick, clicked row id id); });注意这里的id参数来自CursorAdapter内部对_id列的读取如果_id不存在这个回调根本不会正常触发。所以点击能拿到正确 ID是修复成功的强证据。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth排查 SQLite 报错的过程中如果你同时用模型辅助可能会撞上另一类问题——API 调用本身的报错。这两类问题容易混在一起让人以为是代码逻辑错了。我把常见的几个列出来对照。5.1 401 Unauthorized{error:{message:Invalid API key,type:invalid_request_error}}原因Key 写错、过期、或者复制时带了空格。检查Authorization: Bearer sk-xxxx里的 Key 是否完整。注意 Key 只在创建时显示一次如果你没存只能重新生成一个。5.2 local proxy failed / connection refusedError: connect ECONNREFUSED 127.0.0.1:7890原因你的工具或 IDE 插件配置了本地代理端口但那个端口没有服务在监听。检查工具的代理设置把 Base URL 直接指向https://taotoken.net/api不要经过本地转发。这类报错和 SQLite 无关别去改数据库代码。5.3 reading choices 相关报错TypeError: Cannot read properties of undefined (reading choices)原因请求返回的结构不是预期的 OpenAI 格式通常是 Base URL 拼错导致打到了别的端点或者模型 ID 写错导致返回了错误对象。检查三件套Base URL 是否为https://taotoken.net/apiModel ID 是否在平台支持的列表里请求体是否符合协议。5.4 OAuth 相关报错如果你用的是 Claude Code 这类工具可能会遇到 OAuth 授权失败。这类工具通常需要单独配置参考文档https://taotoken.net/doc 。Claude Code 的接入方式在 https://taotoken.net/ClaudeCodeAnthropic 有说明按文档走一遍别自己猜参数。5.5 回到 SQLite 本身三个高频错误第一from数组和投影列名不一致。投影写id AS _idAdapter 的from却写id照样崩。第二用了SELECT *但表里主键叫idCursor 里没有_id。第三ContentProvider拦截了带别名的投影。这三个我都遇到过排查时按顺序过一遍。6. 把模型接进你的 Android 排查流程写到这里SQLite 的修复路径已经完整了。最后说下怎么把 TaoToken 真正用起来而不是配完就忘。如果你只是偶尔查报错用模型对话页最省事https://taotoken.net/models 。把堆栈和代码贴进去让它给你定位层次。如果你想把模型接进 Android Studio 的插件、或者接进自己的脚本做批量代码检查那就需要 API Key 和接入文档https://taotoken.net/api-keys 配合 https://taotoken.net/doc 。文档里有不同语言的调用示例照着改 Base URL 和 Key 就行。对于长期写 Android、经常和 Cursor、Adapter、SQLite 打交道的场景Coding Plan 更合适https://taotoken.net/coding-plan 。它面向持续编码适合把模型当成日常工具而不是临时查询。我自己的习惯是遇到column _id does not exist这类框架约定问题先让模型解释清楚「谁在什么时机调用了getColumnIndexOrThrow」理解机制之后再改代码比直接抄答案记得牢。下次遇到column xxx does not exist的变体你也能自己推出来是投影没对上还是表结构没对上。最后留一个实用技巧在项目里加一个Cursor校验工具方法所有要交给 Adapter 的 Cursor 都先过一遍提前把列名问题暴露出来别等到运行时崩在用户手机上。public static void assertHasId(Cursor cursor) { if (cursor.getColumnIndex(_id) 0) { throw new IllegalStateException( Cursor 缺少 _id 列实际列名: Arrays.toString(cursor.getColumnNames())); } }把它放在 Adapter 构造之前调用报错信息里直接带上实际列名排查时间能从半小时缩到一分钟。