14、SQLite数据库:创建数据库、SQLiteOpenHelper、增删改查、Cursor使用
说实话,Android 本地存储的方案有好几种。SharedPreferences 适合存点配置,File 适合存文件。但真要存结构化的、需要查询的数据,SQLite 才是正经选择。我早期做项目时,总想着用第三方库省事,后来发现不懂原生 SQLite,出了问题连排查方向都没有。嗯,这一章咱们就把 SQLite 的底子打扎实。
本章核心知识点
- SQLite 数据库的创建与升级
- SQLiteOpenHelper 的正确使用姿势
- 增删改查(CRUD)的标准写法
- Cursor 的遍历与资源释放
14.1 创建数据库:SQLiteOpenHelper
Android 官方推荐用 SQLiteOpenHelper 来管理数据库。你想想看,如果每次自己写 openOrCreateDatabase,版本升级时还得手动处理表结构变更,多麻烦。Helper 帮我们封装了这些。
我个人习惯把 Helper 写成单例。为什么?因为数据库连接是资源密集型操作,重复创建开销很大。我曾经在一个项目里看到同事每次查询都 new 一个 Helper,结果内存里同时开了好几个数据库连接,直接 OOM 了。
public class DBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "myapp.db";
private static final int DB_VERSION = 1;
private static DBHelper instance;
private DBHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
public static synchronized DBHelper getInstance(Context context) {
if (instance == null) {
instance = new DBHelper(context.getApplicationContext());
}
return instance;
}
@Override
public void onCreate(SQLiteDatabase db) {
String sql = "CREATE TABLE IF NOT EXISTS user (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT," +
"name TEXT NOT NULL," +
"age INTEGER," +
"phone TEXT)";
db.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 生产环境应该用 ALTER TABLE,而不是直接 drop
db.execSQL("DROP TABLE IF EXISTS user");
onCreate(db);
}
}
💡 个人经验
onUpgrade 里直接 DROP 表再重建,只适合开发阶段。线上版本一定要用 ALTER TABLE 或者建临时表迁移数据。我吃过这个亏——有一次升级没注意,用户数据全丢了,被运营追着骂了一周。
14.2 增删改查(CRUD)
数据库建好了,接下来就是最常用的四个操作。说白了,增删改查就是 app 和数据库对话的基本方式。
14.2.1 插入数据
我推荐用 ContentValues 配合 insert 方法。为什么不用 execSQL 拼 SQL?因为 ContentValues 能自动处理类型转换和 SQL 注入问题。
public long insertUser(String name, int age, String phone) {
SQLiteDatabase db = DBHelper.getInstance(context).getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", name);
values.put("age", age);
values.put("phone", phone);
long id = db.insert("user", null, values);
db.close();
return id;
}
14.2.2 查询数据
查询是 CRUD 里最灵活的。你可以用 query 方法,也可以直接写 SQL。我个人习惯用 query,因为参数清晰,不容易写错。
public Cursor queryUsersByName(String name) {
SQLiteDatabase db = DBHelper.getInstance(context).getReadableDatabase();
String[] columns = {"id", "name", "age", "phone"};
String selection = "name LIKE ?";
String[] selectionArgs = new String[]{"%" + name + "%"};
return db.query("user", columns, selection, selectionArgs, null, null, "age DESC");
}
⚠️ 注意
查询返回的 Cursor 一定要记得关闭!我见过太多人只查不关,导致 Cursor 泄漏,最终数据库无法写入。Cursor 用完后调用 cursor.close(),或者在 finally 块里关。
14.2.3 更新与删除
更新和删除的套路差不多,都是指定条件,然后执行操作。
public int updateUserAge(int id, int newAge) {
SQLiteDatabase db = DBHelper.getInstance(context).getWritableDatabase();
ContentValues values = new ContentValues();
values.put("age", newAge);
int rows = db.update("user", values, "id = ?", new String[]{String.valueOf(id)});
db.close();
return rows;
}
public int deleteUser(int id) {
SQLiteDatabase db = DBHelper.getInstance(context).getWritableDatabase();
int rows = db.delete("user", "id = ?", new String[]{String.valueOf(id)});
db.close();
return rows;
}
14.3 Cursor 的正确使用
Cursor 说白了就是一个指向结果集的指针。你想想看,数据库查询可能返回成千上万条记录,不可能一次性全加载到内存里。Cursor 就是让你一条一条地读。
使用 Cursor 有几个关键点:
- 移动指针:
moveToFirst()移动到第一条,moveToNext()移动到下一条 - 读取数据:
getString(index)、getInt(index)等,index 从 0 开始 - 获取列索引:用
getColumnIndex("name")代替硬编码数字,更安全 - 关闭资源:一定要在 finally 里关闭 Cursor 和数据库
public List<User> getAllUsers() {
List<User> userList = new ArrayList<>();
Cursor cursor = null;
SQLiteDatabase db = null;
try {
db = DBHelper.getInstance(context).getReadableDatabase();
cursor = db.query("user", null, null, null, null, null, null);
while (cursor.moveToNext()) {
User user = new User();
user.setId(cursor.getInt(cursor.getColumnIndex("id")));
user.setName(cursor.getString(cursor.getColumnIndex("name")));
user.setAge(cursor.getInt(cursor.getColumnIndex("age")));
user.setPhone(cursor.getString(cursor.getColumnIndex("phone")));
userList.add(user);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
if (db != null) {
db.close();
}
}
return userList;
}
🔑 关键提醒
Cursor 的 getColumnIndex 返回 -1 时表示列不存在。我曾经在重构表结构时忘了改查询代码,结果一直拿不到数据,排查了半天才发现列名写错了。所以建议查询前先打印列名列表,确认一下。
14.4 事务处理
批量操作时,事务能大幅提升性能。你想想看,如果插入 1000 条数据,每条都自动提交一次,那磁盘 I/O 得多频繁。用事务把 1000 条打包成一个批次提交,速度能快几十倍。
public void batchInsertUsers(List<User> users) {
SQLiteDatabase db = DBHelper.getInstance(context).getWritableDatabase();
db.beginTransaction();
try {
for (User user : users) {
ContentValues values = new ContentValues();
values.put("name", user.getName());
values.put("age", user.getAge());
values.put("phone", user.getPhone());
db.insert("user", null, values);
}
db.setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
} finally {
db.endTransaction();
db.close();
}
}
💡 避坑指南
我曾经在事务里忘了调用 setTransactionSuccessful(),结果所有插入都没生效。记住:beginTransaction 和 endTransaction 之间,一定要显式标记成功,否则默认回滚。
14.5 数据库升级策略
app 版本迭代,数据库结构难免要变。我建议的升级策略是:
| 版本变化 | 推荐做法 | 说明 |
|---|---|---|
| 新增表 | 在 onUpgrade 里执行 CREATE TABLE | 用 IF NOT EXISTS 防止重复创建 |
| 新增列 | ALTER TABLE ADD COLUMN | 注意新列不能有 NOT NULL 约束(除非有默认值) |
| 修改列类型 | 建临时表迁移数据 | SQLite 不支持直接修改列类型 |
| 删除列 | 建临时表迁移数据 | SQLite 不支持直接删除列 |
嗯,到这里 SQLite 的核心操作就讲完了。记住:数据库操作一定要放在子线程,别在主线程里搞。Cursor 和数据库连接用完就关,别偷懒。这些习惯养成了,后面用 Room 或者其他 ORM 框架时,你也能理解底层到底在干什么。
公众号:蓝海资料掘金营,微信deep3321