Room数据库迁移未正确处理ALTER TABLE迁移。

64

Java.lang.IllegalStateException
Migration未正确处理用户(therealandroid.github.com.roomcore.java.User)。

期望:

TableInfo{name='user', columns={name=Column{name='name', type='TEXT', notNull=false, primaryKeyPosition=0}, age=Column{name='age', type='INTEGER', notNull=true, primaryKeyPosition=0}, id=Column{name='id', type='INTEGER', notNull=true, primaryKeyPosition=1}}, foreignKeys=[]} 发现:

TableInfo{name='user', columns={name=Column{name='name', type='TEXT', notNull=false, primaryKeyPosition=0}, id=Column{name='id', type='INTEGER', notNull=true, primaryKeyPosition=1}, age=Column{name='age', type='INTEGER', notNull=false, primaryKeyPosition=0}}, foreignKeys=[]}

我正在尝试执行一个简单的迁移,我有一个名为User的类,它有两个列ID(主键)NAME TEXT,然后我使用两个用户数据填充数据库,然后我在对象User中添加了AGE列,并在Migration常量中添加了alter table以添加此新列,最后我将数据库版本从1替换为2。

这是代码:

User.class

@Entity(tableName = "user")
  public class User {

  @PrimaryKey
  private int id;

  @ColumnInfo(name = "name")
  private String name;

  @ColumnInfo(name = "age")
  private int age;


  public int getId() {
      return id;
  }

  public void setId(int id) {
      this.id = id;
  }

  public String getName() {
      return name;
  }

  public void setName(String name) {
      this.name = name;
  }

  public int getAge() {
      return age;
  }

  public void setAge(int age) {
      this.age = age;
  }
}

数据库类

@Database(entities = {User.class}, version = 2)
public abstract class RoomDatabaseImpl extends RoomDatabase {
    abstract UserDao userDao();
}

迁移代码

public static Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE 'user' ADD COLUMN 'age' INTEGER");
    }
 };
Room.databaseBuilder(context, RoomDatabaseImpl.class, "Sample.db")
            .addMigrations(MIGRATION_1_2)
            .allowMainThreadQueries()
            .build();

在更改对象并添加AGE之前,我添加了两个寄存器并且它工作正常。

执行迁移后,我只是尝试添加一个新的用户,如下所示:

  User user = new User();
  user.setName("JoooJ");
  user.setId(3);
  user.setAge(18);

  List<User> userList = new ArrayList<>();
  userList.add(user);
  App.database(this).userDao().insertAll(userList);  // The crash happens here

其他信息:

使用的是Android Studio 3,我没有在实际环境中进行测试。

依赖项:

compile "android.arch.persistence.room:runtime:1.0.0-alpha9-1"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha9-1"

compile "android.arch.persistence.room:rxjava2:1.0.0-alpha9-1"
gradle 2.3.3

有人可以帮帮我吗?我真的不知道我做错了什么,或者这是一个bug。


2
有点冒险,但也许可以尝试 "ALTER TABLE 'user' ADD COLUMN 'age' INTEGER NOT NULL DEFAULT 0"(0 可以是你认为合适的任何值)。 - MikeT
1
Room 期望列的顺序与字段的顺序匹配。看起来 ALTER TABLE 的结果导致了不同的顺序。 - CommonsWare
请查看以下答案:https://dev59.com/4qbja4cB1Zd3GeqPcjTd#51245898 - Md. Sajedul Karim
@CommonsWare 列的顺序由什么决定? - Skj
@Skj:我已经有几年没用Room了,但是如果我没记错的话,列的顺序应该与实体类中属性的顺序相同。你可以查看生成的SQL或Room创建的数据库以查看列的顺序。 - CommonsWare
9个回答

132

错误信息难以解析,但存在差异:

TableInfo{name='user', columns={name=Column{name='name', type='TEXT', notNull=false, primaryKeyPosition=0}, age=Column{name='age', type='INTEGER', notNull=true, primaryKeyPosition=0}, id=Column{name='id', type='INTEGER', notNull=true, primaryKeyPosition=1}}, foreignKeys=[]} 找到:

找到

TableInfo{name='user', columns={name=Column{name='name', type='TEXT', notNull=false, primaryKeyPosition=0}, id=Column{name='id', type='INTEGER', notNull=true, primaryKeyPosition=1}, age=Column{name='age', type='INTEGER', notNull=false, primaryKeyPosition=0}}, foreignKeys=[]}

Age可为空,但Room预期为非空。

请将您的迁移更改为:

database.execSQL("ALTER TABLE 'user' ADD COLUMN 'age' INTEGER NOT NULL");

由于这个异常解释非常难以解析,我创建了一个小脚本来为您进行比较。

示例:

mig "java.lang.IllegalStateException: Migration failed. expected:TableInfo{name='user', columns={name=Column{name='name', type='TEXT', notNull=false, primaryKeyPosition=0}, age=Column{name='age', type='INTEGER', notNull=true, primaryKeyPosition=0}, id=Column{name='id', type='INTEGER', notNull=true, primaryKeyPosition=1}}, foreignKeys=[]} , found:TableInfo{name='user', columns={name=Column{name='name', type='TEXT', notNull=false, primaryKeyPosition=0}, id=Column{name='id', type='INTEGER', notNull=true, primaryKeyPosition=1}, age=Column{name='age', type='INTEGER', notNull=false, primaryKeyPosition=0}}, foreignKeys=[]}"

结果:

期望值/实际值 差异


5
非常有效,感谢你的帮助。现在我明白了,在执行迁移时,必须指定要填充新列的默认值。 - diogojme
在虚拟数据库上尝试您的迁移,并查看查询是否按预期执行。 - Benoit Duffez
3
我还发现了一件有趣的事情:如果你在模型的@Entity注释中创建了索引,那么你需要在迁移脚本中的SQL语句中也这样做,例如CREATE INDEX index_History_nodeIdONHistory (nodeId),否则会出现“迁移未正确处理”的错误提示。 - Raphael C
嗨,我遇到了一个期望类型为'TEXT'但实际类型为'nvarchar'的错误。在Java中如何表示'type=nvarchar'? - Sandeep Yohans
1
请在alter查询中为添加的列末尾添加默认值“some_default_value”,否则您将会遇到与迁移相关的其他异常,例如“Cannot add a NOT NULL column with default value NULL”。 - Anand Kumar Jha

48

我也写了一个小的JS脚本,你可以在https://hrankit.github.io/RoomSQLiteDifferenceFinder/找到它。

这个过程非常简单。

  1. 在左边的Expected列中输入预期的错误日志。

  2. 在右边的Found列中输入找到的错误日志。

  3. 点击Go按钮。错误日志将被转换为JSON。

  4. 点击Compare按钮,Voila,您就获得了所需的差异。

该插件可以查找Android Studio Logcat中两个Expected和Found dump之间的差异。

查看比较图像:


2
虽然这理论上回答了问题,但最好在此处包含答案的基本部分,并提供参考链接。 - Adriaan
1
@HRankit 真是太棒了,你的脚本解决了我的问题。 - Ritesh Adulkar
2
这个工具现在无法使用,您能否重新检查一下? - Sandeep Yohans
@RiteshAdulkar 如果现有的sqlite表具有VARCHAR列,而新表具有TEXT列,迁移时会有什么区别吗? - Ashish Kanswal
嗨@HRankit,我很感激这个工具,但是我无法让它识别我的粘贴是否合法。它会因为同样的错误信息而被拒绝,而我应该按特定格式进行粘贴。当粘贴失败时,你能否提供更多的调试信息? - Sasquatch
@HRankit,这个工具现在无法使用,尽管我输入了合法的数据。它会抛出一个错误,提示“Json 无效,请从预期的 TableInfo 中粘贴”。 - Umesh P

26

在任何链接中都没有正确的答案。经过多次试验,找到了一种方法。为了使其正常工作,需要按以下方式编写ALTER查询:

None of the answers are correct in any of the links. After much experimentation, a solution has been found. The ALTER query needs to be written in the following way to make it work:

database.execSQL("ALTER TABLE 'user' ADD COLUMN 'age' INTEGER NOT NULL DEFAULT 0")

然而,整数类型的默认值可以是任何值。

如果您想要添加字符串类型的列,请按以下方式添加:

database.execSQL("ALTER TABLE 'user' ADD COLUMN 'address' TEXT")

这个效果棒极了。


我可以给字符串列添加默认值吗? - Mkurbanov

7

今天我遇到了这个问题,我只是在Entities中将int字段更改为Integer。因为int不能为null,但Integer对象可以为null。


引起了一个奇怪的问题,对我来说产生了一个“错误:不可比较类型:int和<null>”的单独问题。 - Prof

5

如果您想添加整型列,请添加以下代码

database.execSQL("ALTER TABLE users"
                    + " ADD COLUMN year INTEGER NOT NULL DEFAULT 0 ")

1
@HRankit 在他的回答中提到的工具 link 今天我尝试使用时似乎没有起效。如果你也有同样的情况,请继续阅读:
如果任何人遇到了问题中所提到的错误信息,并且拥有一个包含大量列的巨型表格,您可以尝试使用this在线工具来检查预期和找到的架构之间的差异。
此外,在生成 <db_version_number>.json(例如:13.json)文件之前,您需要决定列名和数据类型。如果 json 文件已经生成并且你在 Entity 类之后对其进行了更改,则应该删除 json 文件并重新构建项目以生成带有正确值集的文件。
最后,您应该检查迁移本身的 SQL 语句。

1
差异检查器链接实际上有助于比较预期和找到的数据库模式。注意:当使用“布尔”列作为新列来修改表时,必须在ALTER命令中使用INTEGER。请参考此链接:https://stackoverflow.com/a/55304942/5582162 - undefined

1
如果出现notNull差异,可以使用@NonNull注解标记类字段或使用ALTER TABLE更改sql语句。但是,如果出现列类型差异,例如期望的类型为TYPE=TEXT,但发现TYPE=''(COLLATE NOCASE),或期望的类型为INTEGER,但找到了INT,则唯一的解决方案是删除并重新创建表。Sqlite不允许更改列类型。
在Sqlite中使用INTEGER而不是INT,并使用@ColumnInfo(collate = NOCASE)注解标记Java实体(如果在Sqlite中使用NOCASE)。
查看app\schemas下的json文件以获取所需的查询的sql。
static final Migration MIGRATION_2_3= new Migration(2, 3) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {

            database.execSQL("DROP TABLE IF EXISTS table_tmp");

            database.execSQL("CREATE TABLE IF NOT EXISTS `table_tmp` ...");

            database.execSQL("insert into table_tmp (`id`, `name` , ...");

            database.execSQL("DROP INDEX IF EXISTS `index_table_name`");

            database.execSQL("CREATE INDEX IF NOT EXISTS `index_table_name` ON `table_tmp` (`name`)");

            database.execSQL("DROP TABLE IF EXISTS table");

            database.execSQL("alter table table_tmp rename to table");

        }
    };

0

我在使用roomVersion '2.4.0-alpha01'时遇到了问题,这个版本没有为我的情况生成索引表

@Entity(
    tableName = "SoundRules",
    indices = [
        Index(value = ["remoteId"], unique = true)
    ]
) 

我解决了问题,只需将房间版本更新为'2.4.0-alpha03'


0

我在Kotlin中遇到了notNull的差异,并发现了以下异常

Expected:
TableInfo{name='enDic', columns={definition=Column{name='definition', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, _id=Column{name='_id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, defaultValue='null'}, favourite=Column{name='favourite', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, word=Column{name='word', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, usage=Column{name='usage', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}
Found:
TableInfo{name='enDic', columns={usage=Column{name='usage', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=0, defaultValue='null'}, definition=Column{name='definition', type='text', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, _id=Column{name='_id', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null'}, favourite=Column{name='favourite', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=0, defaultValue='null'}, word=Column{name='word', type='text', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}

接下来,我使用下面的代码解决了这个问题

@Entity(tableName = "enDic")
data class Word(

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "_id")
    var _id: Int?,

    @ColumnInfo(name = "word")
    var word: String?,

    @ColumnInfo(name = "definition")
    var definition: String,

    @ColumnInfo(name = "favourite")
    var favourite: Int?,

    @ColumnInfo(name = "usage")
    var usage: Int?

)

不要使用这段代码

@Entity(tableName = "enDic")
data class Word(

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "_id")
    var _id: Int,

    @ColumnInfo(name = "word")
    var word: String,

    @ColumnInfo(name = "definition")
    var definition: String,

    @ColumnInfo(name = "favourite")
    var favourite: Int,

    @ColumnInfo(name = "usage")
    var usage: Int

)

两个@Entity类之间有什么区别? - Nilabja
@Nilabja 我使用了 _id、word、favourite 和 usage 列,它们的变量类型为 null。你可以看到 ? 符号。 - Dilanka Laksiri

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接