对 Room 2.1.0 后升级的 defaultValue 迁移要求感到困惑

Posted

技术标签:

【中文标题】对 Room 2.1.0 后升级的 defaultValue 迁移要求感到困惑【英文标题】:Confusion on the defaultValue migration requirement for upgrade after Room 2.1.0 【发布时间】:2020-04-28 17:05:01 【问题描述】:

Room 2.1.0中,常见的代码如下

版本 2

@Entity(tableName = "password")
public class Password 
    @ColumnInfo(name = "dummy0")
    @NonNull
    public String dummy0;



public class Migration_1_2 extends Migration 
    public Migration_1_2() 
        super(1, 2);
    

    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) 
        database.execSQL("ALTER TABLE password ADD COLUMN dummy0 TEXT NOT NULL DEFAULT ''");
    


来自

的迁移指南 https://developer.android.com/training/data-storage/room/migrating-db-versions.md#handle-default-values-migrations https://developer.android.com/jetpack/androidx/releases/room

相当混乱。

注意:如果您的数据库架构已经有默认值,例如 通过 ALTER TABLE x ADD COLUMN y INTEGER NOTNULL DEFAULT z 添加的那些, 并且您决定通过 @ColumnInfo 将默认值定义为相同的 列,那么您可能需要提供迁移以验证 未计算的默认值。有关详细信息,请参阅房间迁移。


升级到2.2.3之前,有2种可能

    如果迁移运行,我们有一个带有默认值的 dummy0 列。 或者,如果这是新数据库,我们有一个没有默认值的 dummy0 列。

当我们将 Room 2.1.0 升级到 Room 2.2.3 时,这两种情况的一切仍然正常,无需添加额外的迁移代码,用于删除和重新创建表。

我们会做进一步的测试。

版本 3

@Entity(tableName = "password")
public class Password 
    @ColumnInfo(name = "dummy0")
    @NonNull
    public String dummy0;

    @ColumnInfo(name = "dummy1")
    @NonNull
    public String dummy1;


public class Migration_2_3 extends Migration 
    public Migration_2_3() 
        super(2, 3);
    

    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) 
        database.execSQL("ALTER TABLE password ADD COLUMN dummy1 TEXT NOT NULL DEFAULT ''");
    

仍然可以正常工作。

版本 4

@Entity(tableName = "password")
public class Password 
    @ColumnInfo(name = "dummy0")
    @NonNull
    public String dummy0;

    @ColumnInfo(name = "dummy1")
    @NonNull
    public String dummy1;

    @ColumnInfo(name = "dummy2", defaultValue = "")
    @NonNull
    public String dummy2;


public class Migration_3_4 extends Migration 
    public Migration_3_4() 
        super(3, 4);
    

    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) 
        database.execSQL("ALTER TABLE password ADD COLUMN dummy2 TEXT NOT NULL DEFAULT ''");
    

仍然可以正常工作。


所以,我很困惑?在什么用例下,我们需要实际删除并重新创建表?

【问题讨论】:

【参考方案1】:

我认为问题不在于添加新列时,而在于是否将默认值应用/更改/删除到现有列。那时您可能必须重新创建受影响的表。

例如如果你改变了:-

@ColumnInfo(name = "dummy0")
@NonNull
public String dummy0;

添加默认值

@ColumnInfo(name = "dummy0", defaultValue = "")
@NonNull
public String dummy0;

然后会出现架构不匹配,因为预期的架构将具有DEFAULT '',而找到的架构(原始数据库)没有默认编码。

这需要删除并重新创建表,因为您无法通过 ALTER 更改列的属性。

如果在 2.2.0 之前,您有一个以前的非房间生成架构,其中包含默认值并且实体没有相应地更改,那么您会遇到冲突,因为预期架构没有默认值,而找到的架构包含DEFAULT = ''.

这需要相应地更改实体。

示例

假设当前实体是:-

@Entity(tableName = "password")
public class Password 
    @PrimaryKey
    public Long id;
    @ColumnInfo(name = "dummy0")
    @NonNull
    public String dummy0;

    @ColumnInfo(name = "dummy1")
    @NonNull
    public String dummy1;

那么生成的创建表的代码是:-

_db.execSQL("CREATE TABLE IF NOT EXISTS `password` (`id` INTEGER, `dummy0` TEXT NOT NULL, `dummy1` TEXT NOT NULL, PRIMARY KEY(`id`))");
应用已运行上述创建数据库。

如果现在版本 2 更改为:-

@Entity(tableName = "password")
public class Password 
    @PrimaryKey
    public Long id;
    @ColumnInfo(name = "dummy0", defaultValue = "" /*<<<<<<<<<< ADDED */)
    @NonNull
    public String dummy0;

    @ColumnInfo(name = "dummy1")
    @NonNull
    public String dummy1;

那么生成的代码是:-

_db.execSQL("CREATE TABLE IF NOT EXISTS `password` (`id` INTEGER, `dummy0` TEXT NOT NULL DEFAULT '', `dummy1` TEXT NOT NULL, PRIMARY KEY(`id`))");

使用哑/空迁移 (1-2) 运行,然后:-

找到的模式(原始数据库)有:-defaultValue='null' 但预期的架构有:- defaultValue=''''

根据:-

2020-01-11 19:11:15.300 12539-12539/a.so59691979 E/AndroidRuntime: FATAL EXCEPTION: main
    Process: a.so59691979, PID: 12539
    java.lang.RuntimeException: Unable to start activity ComponentInfoa.so59691979/a.so59691979.MainActivity: java.lang.IllegalStateException: Migration didn't properly handle: password(a.so59691979.Password).
     Expected:
    TableInfoname='password', columns=dummy0=Columnname='dummy0', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='''', dummy1=Columnname='dummy1', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null', id=Columnname='id', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null', foreignKeys=[], indices=[]
     Found:
    TableInfoname='password', columns=dummy0=Columnname='dummy0', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null', dummy1=Columnname='dummy1', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null', id=Columnname='id', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null', foreignKeys=[], indices=[]
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3270)

示例修复

使用迁移:-

Migration M1_2 = new Migration(1,2) 
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) 

        // CREATE SQL Copied from the generated Java PasswordDatabase_Impl (name changed)
        final String SQL_CREATE_NEW_PASSWORDTABLE =
                "CREATE TABLE IF NOT EXISTS `password_new` (`id` INTEGER, `dummy0` TEXT NOT NULL DEFAULT '', `dummy1` TEXT NOT NULL, PRIMARY KEY(`id`))";
        database.execSQL(SQL_CREATE_NEW_PASSWORDTABLE);
        database.execSQL("INSERT INTO `password_new` SELECT * FROM `password`");
        database.execSQL("ALTER TABLE `password` RENAME TO `password_old`");
        database.execSQL("ALTER TABLE `password_new` RENAME TO `password`");
        database.execSQL("DROP TABLE IF EXISTS `password_old`");
    

解决问题。

代码

以下代码用于生成上述内容:-

Password.java

/*
//Original
@Entity(tableName = "password")
public class Password 
    @PrimaryKey
    public Long id;
    @ColumnInfo(name = "dummy0")
    @NonNull
    public String dummy0;

    @ColumnInfo(name = "dummy1")
    @NonNull
    public String dummy1;

*/

// New
@Entity(tableName = "password")
public class Password 
    @PrimaryKey
    public Long id;
    @ColumnInfo(name = "dummy0", defaultValue = "" /*<<<<<<<<<< ADDED */)
    @NonNull
    public String dummy0;

    @ColumnInfo(name = "dummy1")
    @NonNull
    public String dummy1;

最初使用的是原版

PasswordDatabase.java

@Database(version = 2, entities = Password.class)
public abstract class PasswordDatabase extends RoomDatabase 

初始版本为 1

MainActivity.java

公共类 MainActivity 扩展 AppCompatActivity

PasswordDatabase passwordDatabase;

@Override
protected void onCreate(Bundle savedInstanceState) 
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    passwordDatabase = Room.databaseBuilder(
            this,
            PasswordDatabase.class,
            "passworddb"
    )
            .allowMainThreadQueries()
            .addMigrations(M1_2)
            .build();
    passwordDatabase.getOpenHelper().getWritableDatabase();


Migration M1_2 = new Migration(1,2) 
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) 

        // CREATE SQL Copied from the generated Java PasswordDatabase_Impl (name changed)
        final String SQL_CREATE_NEW_PASSWORDTABLE =
                "CREATE TABLE IF NOT EXISTS `password_new` (`id` INTEGER, `dummy0` TEXT NOT NULL DEFAULT '', `dummy1` TEXT NOT NULL, PRIMARY KEY(`id`))";
        database.execSQL(SQL_CREATE_NEW_PASSWORDTABLE);
        database.execSQL("INSERT INTO `password_new` SELECT * FROM `password`");
        database.execSQL("ALTER TABLE `password` RENAME TO `password_old`");
        database.execSQL("ALTER TABLE `password_new` RENAME TO `password`");
        database.execSQL("DROP TABLE IF EXISTS `password_old`");
    
;
最初M1_2的主体是空的(以强制出错)

【讨论】:

以上是关于对 Room 2.1.0 后升级的 defaultValue 迁移要求感到困惑的主要内容,如果未能解决你的问题,请参考以下文章

刷安卓room有啥用??

记账本APP小升级

添加 androidx.room:room-compiler:2.1.0-alpha05 后项目未编译

Android jetpack room 记录数据库升级日志

Android 使用Room操作数据库进行数据库版本的升级和迁移

Android jetpack room 数据库的升级异常处理