14、SQLite数据库:创建数据库、SQLiteOpenHelper、增删改查、Cursor使用

说实话,Android 本地存储的方案有好几种。SharedPreferences 适合存点配置,File 适合存文件。但真要存结构化的、需要查询的数据,SQLite 才是正经选择。我早期做项目时,总想着用第三方库省事,后来发现不懂原生 SQLite,出了问题连排查方向都没有。嗯,这一章咱们就把 SQLite 的底子打扎实。

本章核心知识点

  • SQLite 数据库的创建与升级
  • SQLiteOpenHelper 的正确使用姿势
  • 增删改查(CRUD)的标准写法
  • Cursor 的遍历与资源释放
SQLite 数据库知识体系 SQLiteOpenHelper 创建/升级数据库 增删改查 (CRUD) Cursor 遍历 事务与性能 onCreate / onUpgrade insert / update / delete / query moveToFirst / getString / close beginTransaction / endTransaction 图:SQLite 数据库操作核心模块

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(),结果所有插入都没生效。记住:beginTransactionendTransaction 之间,一定要显式标记成功,否则默认回滚。

14.5 数据库升级策略

app 版本迭代,数据库结构难免要变。我建议的升级策略是:

版本变化 推荐做法 说明
新增表 在 onUpgrade 里执行 CREATE TABLE 用 IF NOT EXISTS 防止重复创建
新增列 ALTER TABLE ADD COLUMN 注意新列不能有 NOT NULL 约束(除非有默认值)
修改列类型 建临时表迁移数据 SQLite 不支持直接修改列类型
删除列 建临时表迁移数据 SQLite 不支持直接删除列

嗯,到这里 SQLite 的核心操作就讲完了。记住:数据库操作一定要放在子线程,别在主线程里搞。Cursor 和数据库连接用完就关,别偷懒。这些习惯养成了,后面用 Room 或者其他 ORM 框架时,你也能理解底层到底在干什么。


公众号:蓝海资料掘金营,微信deep3321