Room 2.1.0之后升级defaultValue迁移需求的困惑

Confusion on the defaultValue migration requirement for upgrade after Room 2.1.0

在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 ''");
    }
}

来自

的迁移指南

很混乱。

Note: If your database schema already has default values, such as those added via ALTER TABLE x ADD COLUMN y INTEGER NOTNULL DEFAULT z, and you decide to define default values via @ColumnInfo to the same columns, then you might need to provide a migration to validate the unaccounted default values. See Room Migrations for more information.


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

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

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

我们会做进一步的测试。

版本 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 ''");
    }
}

仍然工作正常。


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

我认为问题不在于添加新列时,而是在现有列的默认值为 applied/changed/removed 时。然后您可能必须重新创建受影响的 table(s).

例如如果你改变了:-

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

添加默认值

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

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

  • 这需要删除并重新创建 table,因为您无法通过 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;
}

然后创建 table 的生成代码是:-

_db.execSQL("CREATE TABLE IF NOT EXISTS `password` (`id` INTEGER, `dummy0` TEXT NOT NULL, `dummy1` TEXT NOT NULL, PRIMARY KEY(`id`))");
  • App 是 运行 上面创建的数据库。

如果现在版本 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`))");

运行 一次 dumb/empty 迁移 (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 ComponentInfo{a.so59691979/a.so59691979.MainActivity}: java.lang.IllegalStateException: Migration didn't properly handle: password(a.so59691979.Password).
     Expected:
    TableInfo{name='password', columns={dummy0=Column{name='dummy0', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue=''''}, dummy1=Column{name='dummy1', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null'}}, foreignKeys=[], indices=[]}
     Found:
    TableInfo{name='password', columns={dummy0=Column{name='dummy0', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, dummy1=Column{name='dummy1', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='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

public class MainActivity extends 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 的正文是空的(以便强制错误)