7、插入操作详解:insert() 方法实现、返回 URI、批量插入优化
插入操作,说白了就是往 Content Provider 里塞数据。这活儿看起来简单,但坑不少。我见过太多人把 insert() 当成简单的 SQL INSERT 来写,结果要么性能拉胯,要么返回的 URI 格式不对,导致客户端拿不到正确的数据路径。
今天咱们就把 insert() 的里里外外掰扯清楚。从最基础的实现,到返回 URI 的规范,再到批量插入的优化策略,一条龙讲透。
7.1 insert() 方法的标准实现
先看一个最基础的 insert() 实现长什么样。我习惯把模板代码先摆出来,再讲细节。
@Override
public Uri insert(Uri uri, ContentValues values) {
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
Uri returnUri;
switch (match) {
case CODE_ITEMS:
long id = db.insert(TABLE_ITEMS, null, values);
if (id > 0) {
returnUri = ContentUris.withAppendedId(
ItemsEntry.CONTENT_URI, id);
} else {
throw new SQLException("插入失败,uri: " + uri);
}
break;
case CODE_ITEMS_WITH_CATEGORY:
// 带分类参数的插入,稍后讲
throw new UnsupportedOperationException(
"暂不支持子路径插入");
default:
throw new IllegalArgumentException(
"未知 URI: " + uri);
}
getContext().getContentResolver().notifyChange(
returnUri, null);
return returnUri;
}
这段代码有几个关键点,我一个个说。
7.1.1 参数校验不能省
很多新手会忽略 values 为 null 的情况。你想想看,如果客户端传了个 null 进来,db.insert() 会直接返回 -1。这时候你返回的 URI 就是错的。
我个人习惯在方法开头加一道校验:
if (values == null || values.size() == 0) {
throw new IllegalArgumentException("values 不能为空");
}
别嫌麻烦。我在项目中遇到过因为 values 为空导致批量插入全部失败的情况,排查了半天才发现是某个业务方传了空值。
7.1.2 返回 URI 的规范
insert() 返回的 URI 不是随便拼的。它必须遵循 content://authority/path/id 这个格式。为什么?因为客户端拿到这个 URI 后,可以直接用它做后续的查询、更新、删除操作。
举个例子:
// 正确做法
Uri insertedUri = ContentUris.withAppendedId(
ItemsEntry.CONTENT_URI, newId);
// 错误做法
Uri wrongUri = Uri.parse(
"content://com.example.provider/items/" + newId);
用 ContentUris.withAppendedId() 的好处是,它会自动处理 URI 的编码问题。你自己拼字符串,万一 id 里带个特殊字符,就炸了。
7.2 批量插入的三种姿势
批量插入是性能优化的重灾区。我见过有人用 for 循环一条条 insert(),结果插入 1000 条数据花了十几秒。这显然不行。
下面我列出三种常见的批量插入方案,你根据场景选。
| 方案 | 适用场景 | 性能 | 复杂度 |
|---|---|---|---|
| ContentProviderOperation | 中等数据量(几百条) | 中等 | 低 |
| SQLite 事务 + 预编译 | 大数据量(几千条以上) | 高 | 中 |
| bulkInsert() 重写 | 框架级批量插入 | 高 | 高 |
7.2.1 方案一:ContentProviderOperation
这是 Android 官方推荐的方式。客户端用 ContentProviderOperation 构建一个操作列表,然后通过 ContentResolver.applyBatch() 一次性提交。
// 客户端代码
ArrayList<ContentProviderOperation> ops =
new ArrayList<>();
for (ItemData item : itemList) {
ops.add(ContentProviderOperation
.newInsert(ItemsEntry.CONTENT_URI)
.withValues(item.toContentValues())
.build());
}
ContentProviderResult[] results =
getContentResolver().applyBatch(
AUTHORITY, ops);
这个方案的好处是,Provider 端不需要做额外处理。applyBatch() 默认会在一个事务里执行所有操作。但注意,如果数据量太大(比如上万条),这个方案会有点吃力,因为所有操作都在内存里构建。
7.2.2 方案二:SQLite 事务 + 预编译
这是我自己最常用的方案。说白了就是绕过 ContentProvider 的逐条调用,直接在 Provider 内部用 SQLite 事务批量处理。
@Override
public int bulkInsert(Uri uri, ContentValues[] valuesArray) {
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
if (match != CODE_ITEMS) {
throw new IllegalArgumentException(
"bulkInsert 不支持该 URI");
}
db.beginTransaction();
try {
String sql = "INSERT INTO " + TABLE_ITEMS +
" (name, price, category) VALUES (?, ?, ?)";
SQLiteStatement stmt = db.compileStatement(sql);
for (ContentValues values : valuesArray) {
stmt.bindString(1,
values.getAsString("name"));
stmt.bindDouble(2,
values.getAsDouble("price"));
stmt.bindString(3,
values.getAsString("category"));
stmt.executeInsert();
stmt.clearBindings();
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(
uri, null);
return valuesArray.length;
}
这里有个细节:用 SQLiteStatement 预编译,比每次调用 db.insert() 快得多。我实测过,插入 5000 条数据,预编译方案比逐条 insert 快了将近 10 倍。
7.2.3 方案三:重写 bulkInsert()
如果你需要更精细的控制,比如插入前做数据校验、去重,或者插入后更新缓存,那就重写 bulkInsert()。
@Override
public int bulkInsert(Uri uri, ContentValues[] valuesArray) {
int insertedCount = 0;
SQLiteDatabase db = mDbHelper.getWritableDatabase();
db.beginTransaction();
try {
for (ContentValues values : valuesArray) {
// 先检查是否已存在
if (!exists(db, values)) {
long id = db.insert(TABLE_ITEMS, null, values);
if (id > 0) {
insertedCount++;
}
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
if (insertedCount > 0) {
getContext().getContentResolver().notifyChange(
uri, null);
}
return insertedCount;
}
这个方案的好处是灵活。你可以控制哪些数据真正被插入,哪些被跳过。但注意,不要在循环里做太重的操作,比如网络请求。事务会一直持有数据库锁,搞太久会影响其他线程的读写。
7.3 插入操作的避坑指南
嗯,这里我要说几个我踩过的坑。
db.enableWriteAheadLogging()。
7.4 知识体系总览
下面这张图把 insert() 的核心逻辑串起来了。你可以看到从客户端调用到 Provider 处理,再到数据库写入的完整链路。
从图上你可以看到,insert() 不是简单的「客户端→Provider→数据库」单向流程。它还包括 URI 的构建、数据变化的通知,以及批量插入时的多种优化路径。
好了,insert() 的内容就讲到这里。记住三个核心点:URI 要规范、批量要用事务、通知不能忘。下一节我们聊 update() 和 delete(),这两个操作也有不少门道。
公众号:蓝海资料掘金营,微信deep3321