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。
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();
}
}
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);
}
}
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 三者之间的关系。我建议你仔细看看,尤其是数据流向和版本管理的部分。
5.6 实战中的几个坑
最后,分享几个我在项目中真实踩过的坑:
- 多线程并发:SQLiteOpenHelper 内部做了同步处理,但如果你在多个线程里同时调用
getWritableDatabase(),还是可能出问题。建议用单例模式。 - 数据库文件被删除:用户可能在设置里清除数据,导致数据库文件被删。这时候
getWritableDatabase()会重新创建,但你的 Provider 可能还持有旧连接。记得在onCreate()里重新初始化。 - 版本号回退:如果你不小心把版本号改小了,
onDowngrade()会抛异常。默认实现是直接崩溃。要么重写onDowngrade(),要么永远不要回退版本号。
好了,这一章的内容就到这里。记住,数据库操作无小事——尤其是升级那一步,稍有不慎就是数据灾难。下一章我们会聊聊如何用 Loader 异步加载数据,让 UI 不再卡顿。
公众号:蓝海资料掘金营,微信deep3321