背景:
模块线上因版本屏蔽范围遗漏,导致应用出现数据库降级的场景,模拟影响时,发现出现数据库字段丢失问题
场景:数据库版本 v6 → v5 → v7
现象:
表现层看到的是同步上传、下载都出现了失败问题,对应的代码层面的问题,则是数据升级后出现了字段丢失,丢失的字段是升级到version7新增的字段,字段有插入动作,但实际未成功插入
日志和问题代码:
1、低版本升级到v6 数据库升级
2026-01-06 11:14:53.674 29023-29023 _V_VG_Clou...ntDbHelper usap64 D upgrading database from version 4 to 6
2、v6 降级到v5
2026-01-06 11:15:58.934 29397-29397 _V_VG_Clou...ntDbHelper usap64 D onDowngrade database from version 6 to 5
3、v5 升级到 v7
2026-01-06 11:16:33.456 32339-9589 _V_VG_Clou...ntDbHelper usap64 D upgrading database from version 5 to 7
2026-01-06 11:16:33.456 32339-9589 _V_VG_Clou...ntDbHelper usap64 E upgradeToVersion6 start
2026-01-06 11:16:33.457 32339-9589 _V_VG_Clou...ntDbHelper usap64 E upgradeToVersion6 android.database.sqlite.**SQLiteException: duplicate column name: is_attached_file** (code 1 SQLITE_ERROR): , while compiling: ALTER TABLE cloud_gallery ADD COLUMN is_attached_file INTEGER
2026-01-06 11:16:33.457 32339-9589 _V_VG_Clou...ntDbHelper usap64 E upgradeToVersion6 end
2026-01-06 11:16:33.457 32339-9589 _V_VG_Clou...ntDbHelper usap64 E upgradeToVersion7 start
2026-01-06 11:16:33.457 32339-9589 _V_VG_Clou...ntDbHelper usap64 E upgradeToVersion7 end
4、业务触发数据库操作异常,异常字段为upgradeToVersion7新增的字段**
2026-01-06 10:28:24.059 15402-15416 _V_VG_Clou...ntProvider com.xxx D URI_CLOUD_GALLERY_TABLE Exception:android.database.sqlite.SQLiteException: no such column: md5_rename_time (code 1 SQLITE_ERROR):
//数据升级时,按照版本迭代逐步升级如 upgrading database from version 5 to 7,则会执行upgradeToVersion6、upgradeToVersion7 case
//每个case内部都通过 事务来处理,保证同一个版本的字段操作的原子性
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
LogUtil.d(TAG, "upgrading database from version " +
oldVersion + " to " + newVersion);
switch (oldVersion) {
case 1:
upgradeToVersion2(db);
case 2:
upgradeToVersion3(db);
case 3:
upgradeToVersion4(db);
case 4:
upgradeToVersion5(db);
case 5:
upgradeToVersion6(db);
case 6:
upgradeToVersion7(db);
default:
break;
}
}
private void upgradeToVersionX(SQLiteDatabase db) {
db.beginTransaction();
try {
db.execSQL("ALTER TABLE " + CloudAgentSchema.CloudGallery
.CLOUD_GALLERY_TABLE + " ADD COLUMN " + CloudAgentSchema.CloudImageColumns.MD5_RENAME_TIME + " TEXT");
db.setTransactionSuccessful();
} catch (Exception e) {
LogUtil.e(TAG, "upgradeToVersion7 " + e);
} finally {
db.endTransaction();
}
}
原因分析:
怀疑是对事务的API不熟悉,查了相关API的解释, 我们的调用方式和他提供的模版一样,且都是顺序执行,没有嵌套场景,怀疑是源码中存在事务操作,导致触发了外层事务的回滚
总结:
从源码来看:数据库的创建、升级、降级的方法 (onCreate、onUpgrade、onDowngrade) 源码层面都帮我们实现了事务的能力,保证原子性,我们在这些回调内,若自己实现事务,就属于嵌套场景,会因其中的某个事务失败,导致所有的操作都被回滚
原因知道了,该如何修改:
那种方案都有问题,我们最终选择在嵌套事务的基础上,增加了字段是否存在的校验 这里该怎么写呢,看下原生下载管理器的实现方式 --升级场景 直接SQL的形式 --降级场景:删除重建
/**
* Creates and updated database on demand when opening it.
* Helper class to create database the first time the provider is
* initialized and upgrade it when a new version of the provider needs
* an updated version of the database.
*/
private final class DatabaseHelper extends SQLiteOpenHelper {
/**
* Updates the database format when a content provider is used
* with a database that was created with a different format.
*
* Note: to support downgrades, creating a table should always drop it first if it already
* exists.
*/
@Override
public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
VLog.i(TAG, "onUpgrade() oldV: " + oldV + " newV: " + newV);
if (oldV == VERSION_31) {
// 31 and 100 are identical, just in different codelines. Upgrading from 31 is the
// same as upgrading from 100.
oldV = 100;
} else if (oldV < VERSION_100) {
// no logic to upgrade from these older version, just recreate the DB
VLog.i(TAG, "Upgrading downloads database from version " + oldV + " to version " + newV
+ ", which will destroy all old data");
oldV = 99;
} else if (oldV > newV) {
// user must have downgraded software; we have no way to know how to downgrade the
// DB, so just recreate it
VLog.i(TAG, "Downgrading downloads database from version " + oldV + " (current version is " + newV
+ "), destroying all old data");
oldV = 99;
}
for (int version = oldV + 1; version <= newV; version++) {
upgradeTo(db, version);
}
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
VLog.i(TAG, "onDowngrade(): oldVersion=" + oldVersion + ", newVersion=" + newVersion);
db.beginTransaction();
try {
Cursor c = db.query("sqlite_master", new String[]{"name"}, "type=?", new String[]{"table"}, null,
null, null);
String tableName;
try {
while (c != null && c.moveToNext()) {
tableName = c.getString(0);
if ("android_metadata".equals(tableName)) {
continue;
}
VLog.i(TAG, "drop table " + tableName);
db.execSQL("DROP TABLE IF EXISTS " + tableName);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (c != null) {
c.close();
}
}
onCreate(db);
db.setTransactionSuccessful();
} catch (Exception e) {
VLog.e(TAG, "drop all table from master failed, drop manually.");
db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
db.execSQL("DROP TABLE IF EXISTS " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE);
onCreate(db);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
private void upgradeTo(SQLiteDatabase db, int version) {
VLog.i(TAG, "upgradeTo() version: " + version);
switch (version) {
case 102:
addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_IS_PUBLIC_API,
"INTEGER NOT NULL DEFAULT 0");
addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_ROAMING,
"INTEGER NOT NULL DEFAULT 0");
addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES,
"INTEGER NOT NULL DEFAULT 0");
break;
case 103:
addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
"INTEGER NOT NULL DEFAULT 1");
makeCacheDownloadsInvisible(db);
break;
case 104:
addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT,
"INTEGER NOT NULL DEFAULT 0");
break;
case 105:
fillNullValues(db);
break;
case 106:
addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, "TEXT");
addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_DELETED,
"BOOLEAN NOT NULL DEFAULT 0");
break;
case 107:
addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ERROR_MSG, "TEXT");
break;
case 108:
addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_METERED,
"INTEGER NOT NULL DEFAULT 1");
break;
case 109:
addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_WRITE,
"BOOLEAN NOT NULL DEFAULT 0");
break;
case 110:
addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_FLAGS,
"INTEGER NOT NULL DEFAULT 0");
break;
default:
// throw new IllegalStateException("Don't know how to upgrade to " + version);
break;
}
}
private void addColumn(SQLiteDatabase db, String dbTable, String columnName,
String columnDefinition) {
db.execSQL("ALTER TABLE " + dbTable + " ADD COLUMN " + columnName + " "
+ columnDefinition);
}
}