5、SQLite 与 Content Provider 整合:SQLiteOpenHelper 使用、数据库创建与升级、Provider 与数据库绑定

说实话,很多新手把 Content Provider 当成一个「数据中转站」,觉得它只是把数据库包装一下暴露出去。但在我看来,Provider 的灵魂就是 SQLite。你想想看,没有数据库支撑的 Provider,就像没有引擎的跑车——空有个壳子。

这一章,我们就来聊聊怎么把 SQLite 和 Content Provider 真正「焊死」在一起。我会从 SQLiteOpenHelper 的使用讲起,再到数据库的创建与升级,最后完成 Provider 与数据库的绑定。嗯,都是我在项目里踩过坑的地方。

5.1 SQLiteOpenHelper:数据库的管家

我个人习惯把 SQLiteOpenHelper 看作数据库的「管家」。它负责三件事:创建数据库、管理版本、维护连接。你不需要手动调用 getWritableDatabase() 的细节,它全帮你搞定。

来看一个典型的实现:

public class MyDatabaseHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "myapp.db";
    private static final int DATABASE_VERSION = 1;

    private static final String SQL_CREATE_TABLE =
        "CREATE TABLE " + MyContract.TABLE_NAME + " (" +
        MyContract._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
        MyContract.COLUMN_NAME + " TEXT NOT NULL, " +
        MyContract.COLUMN_AGE + " INTEGER)";

    public MyDatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 生产环境千万别这么干!后面会讲正确做法
        db.execSQL("DROP TABLE IF EXISTS " + MyContract.TABLE_NAME);
        onCreate(db);
    }
}

这里有个细节:数据库文件名。我见过有人把数据库名写成 myapp.db,结果多个 Provider 共用同一个名字,直接冲突。建议用包名+功能命名,比如 com.example.myapp.provider.db

我的习惯:把数据库版本号定义成常量,放在 Contract 类里。这样 Provider 和 Helper 都能引用,避免版本号不一致的坑。

5.2 数据库创建:别在 onCreate 里写太多

你可能会想:onCreate 不就是建表吗?有什么好说的?

嗯,我曾经在一个项目里,onCreate 里写了 20 多张表的建表语句。结果第一次安装时,用户等了足足 3 秒才看到界面。为什么?因为 onCreate 是在 UI 线程执行的(如果你没做异步处理)。

正确的做法是:

  • 只建核心表:那些启动就要用的表,比如用户信息、配置表。
  • 延迟建表:非核心表可以在第一次访问时再创建。
  • 使用事务:如果必须建多张表,用 beginTransaction() 包裹。
@Override
public void onCreate(SQLiteDatabase db) {
    db.beginTransaction();
    try {
        db.execSQL(SQL_CREATE_TABLE_USER);
        db.execSQL(SQL_CREATE_TABLE_CONFIG);
        // 其他核心表...
        db.setTransactionSuccessful();
    } finally {
        db.endTransaction();
    }
}
注意:onCreate 里不要做耗时操作,比如网络请求、文件读写。数据库创建本身是 I/O 操作,已经够慢了。

5.3 数据库升级:onUpgrade 的正确姿势

这是最容易出问题的地方。我曾经接手过一个项目,onUpgrade 里直接 DROP TABLE 然后重建。结果用户升级后,所有数据都没了——被骂到怀疑人生。

正确的升级策略是:逐版本迁移。每个版本只做增量修改。

版本号 变更内容 迁移 SQL
1 → 2 新增 age 列 ALTER TABLE user ADD COLUMN age INTEGER
2 → 3 新增 address 表 CREATE TABLE address (...)
3 → 4 修改 phone 列类型 先建新表,再迁移数据,最后删旧表

代码实现:

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    // 逐版本升级,而不是直接跳到最新
    switch (oldVersion) {
        case 1:
            db.execSQL("ALTER TABLE user ADD COLUMN age INTEGER");
            // 注意:这里没有 break,继续执行下一个 case
        case 2:
            db.execSQL("CREATE TABLE address (...)");
        case 3:
            // 复杂迁移:建新表、复制数据、删旧表
            upgradeUserTable(db);
    }
}
关键点:case 语句里不要加 break。这样用户从版本 1 升级到 4 时,会依次执行 1→2、2→3、3→4 的迁移。如果加了 break,版本 1 的用户只会执行 1→2 的迁移,后面的就丢了。

5.4 Provider 与数据库绑定:打通任督二脉

好了,现在数据库有了,Helper 也写好了。怎么把它和 Provider 绑在一起?

说白了,就是在 Provider 的 onCreate() 里初始化 Helper,然后在各个 CRUD 方法里获取数据库实例。

public class MyProvider extends ContentProvider {

    private MyDatabaseHelper dbHelper;

    @Override
    public boolean onCreate() {
        dbHelper = new MyDatabaseHelper(getContext());
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        // 执行查询...
        return cursor;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        long id = db.insert(MyContract.TABLE_NAME, null, values);
        // 通知数据变化...
        return ContentUris.withAppendedId(uri, id);
    }

    // update, delete 类似...
}

这里有个容易被忽略的点:数据库连接的生命周期。Provider 的 onCreate() 是在应用进程启动时调用的,所以 dbHelper 会一直存活。但如果你在 getWritableDatabase() 时遇到异常,记得处理。

避坑指南:我曾经在 insert() 里直接调用 dbHelper.getWritableDatabase(),结果在磁盘满时抛出了异常,导致整个 Provider 崩溃。后来我加了一层 try-catch,并在 catch 里返回 null 或抛出合适的异常。

5.5 核心逻辑流程图

下面这张图展示了 Provider、Helper、SQLite 三者之间的关系。我建议你仔细看看,尤其是数据流向和版本管理的部分。

Content Provider 与 SQLite 整合流程图 Content Provider onCreate() 初始化 query/insert/update/delete SQLiteOpenHelper getReadableDatabase() getWritableDatabase() SQLiteDatabase execSQL() / rawQuery() insert() / update() / delete() 调用 返回 版本管理核心逻辑 onCreate(SQLiteDatabase db) : 首次安装时调用 onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) : 版本升级时调用 onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) : 版本降级时调用(默认抛异常) 触发 最佳实践总结 1. Provider 中只初始化 Helper,不直接操作 Database 2. 版本升级采用逐版本迁移,避免数据丢失 3. 数据库操作放在工作线程,避免 ANR 4. 使用 Contract 类统一管理表名、列名、版本号

5.6 实战中的几个坑

最后,分享几个我在项目中真实踩过的坑:

  • 多线程并发:SQLiteOpenHelper 内部做了同步处理,但如果你在多个线程里同时调用 getWritableDatabase(),还是可能出问题。建议用单例模式。
  • 数据库文件被删除:用户可能在设置里清除数据,导致数据库文件被删。这时候 getWritableDatabase() 会重新创建,但你的 Provider 可能还持有旧连接。记得在 onCreate() 里重新初始化。
  • 版本号回退:如果你不小心把版本号改小了,onDowngrade() 会抛异常。默认实现是直接崩溃。要么重写 onDowngrade(),要么永远不要回退版本号。
一句话总结:SQLiteOpenHelper 是 Provider 的「心脏」,版本管理是「命脉」。把这两块搞扎实了,你的 Provider 才能稳定运行。

好了,这一章的内容就到这里。记住,数据库操作无小事——尤其是升级那一步,稍有不慎就是数据灾难。下一章我们会聊聊如何用 Loader 异步加载数据,让 UI 不再卡顿。


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