记一次数据库升级失败问题分析

26 阅读5分钟

背景:

模块线上因版本屏蔽范围遗漏,导致应用出现数据库降级的场景,模拟影响时,发现出现数据库字段丢失问题

场景:数据库版本 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的解释, 我们的调用方式和他提供的模版一样,且都是顺序执行,没有嵌套场景,怀疑是源码中存在事务操作,导致触发了外层事务的回滚

image.png

image.png

总结:

从源码来看:数据库的创建、升级、降级的方法 (onCreate、onUpgrade、onDowngrade) 源码层面都帮我们实现了事务的能力,保证原子性,我们在这些回调内,若自己实现事务,就属于嵌套场景,会因其中的某个事务失败,导致所有的操作都被回滚

原因知道了,该如何修改:

image.png

那种方案都有问题,我们最终选择在嵌套事务的基础上,增加了字段是否存在的校验 这里该怎么写呢,看下原生下载管理器的实现方式 --升级场景 直接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);
    }
 
}