Can't migrate a table to Room do to an error with the way booleans is saved in Sqlite

Posted

技术标签:

【中文标题】Can\'t migrate a table to Room do to an error with the way booleans is saved in Sqlite【英文标题】:Can't migrate a table to Room do to an error with the way booleans are saved in SqliteCan't migrate a table to Room do to an error with the way booleans is saved in Sqlite 【发布时间】:2019-10-05 05:36:07 【问题描述】:

我一直在尝试将我的应用迁移到 Room。我正在为一个无法直接迁移的特定表而苦苦挣扎,因为它的创建方式。

这些字段是使用数据类型 BOOLBYTE 而不是 INTEGER 创建的。

我已经尝试失败了:

将我的实体字段更改为 Int/Boolean/Byte 时出现相同错误 创建 TypeConverter 以将其保存为布尔值/字节 将typeAffinity 添加为UNDEFINED 在我的亲和度= 1 的实体的@ColumnInfo

我的databaseSQL造句:

CREATE TABLE IF NOT EXISTS myTable (_id INTEGER PRIMARY KEY AUTOINCREMENT,
my_first_field BOOL NOT NULL DEFAULT 0,
my_second_field BYTE NOT NULL DEFAULT 0)

我的实体:

@Entity(tableName = "myTable")
data class MyTable(
        @PrimaryKey(autoGenerate = true)
        @ColumnInfo(name = "_id")
        var id: Int,

        @ColumnInfo(name = "my_first_field")
        var myFirstField: Boolean = false,

        @ColumnInfo(name = "my_second_field")
        var mySecondField: Byte = false
)

我经常遇到的错误是:

Expected:
TableInfoname='my_table', columns=_id=Columnname='_id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, my_first_field=Columnname='my_first_field', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, my_second_field=Columnname='my_second_field', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, foreignKeys=[], indices=[]
     Found:
TableInfoname='my_table', columns=_id=Columnname='_id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, my_first_field=Columnname='my_first_field', type='BOOL', affinity='1', notNull=true, primaryKeyPosition=0, my_second_field=Columnname='my_second_field', type='BYTE', affinity='1', notNull=true, primaryKeyPosition=0, foreignKeys=[], indices=[]

有没有什么方法可以在不创建迁移策略的情况下直接进行?

【问题讨论】:

请添加更多代码! @mohammadRezaAbiri 您认为还有哪些有用/必要的代码? Sqlite 没有“布尔”或“字节”类型...使用整数。 sqlite.org/datatype3.html 我知道 SQLITE 没有布尔值或字节。实际上,如果数据库是新的,则不会发生此错误,因为数据库字段被初始化为 INTEGER 而不是 BYTE 或 BOOL。我的问题是如何避免破坏我的表并将信息复制到新表中。 【参考方案1】:

我相信你可以,构建房间数据库之前:-

    检查是否需要做任何事情,例如通过使用:-

    SELECT count() FROM sqlite_master WHERE name = 'myTable' AND instr(sql,' BOOL ') AND instr(sql,' BYTE ');

    然后检查结果。

    如果它为 0,则什么也不做(尽管为了安全起见,您只能在 oldmyTable 为 0 时使用 DROP TABLE IF EXISTS oldmyTable)。

    如果以上返回 1 则:-

    删除重命名的原始表(见下文和上文)以防万一它存在:-

    DROP TABLE IF EXISTS oldmyTable;

    使用

    定义另一个表

    CREATE TABLE IF NOT EXISTS myOtherTable (_id INTEGER PRIMARY KEY AUTOINCREMENT, my_first_field INTEGER NOT NULL DEFAULT 0, my_second_field INTEGER NOT NULL DEFAULT 0)

    预期的架构

    使用

    填充新表 INSERT INTO myOtherTable SELECT * FROM myTable;

    使用 :-

    重命名 mytable ALTER TABLE mytable RENAME TO oldmyTable;

    使用原始名称重命名 myOtherTable :-

    ALTER TABLE myOtherTable RENAME TO mytable;

    删除重命名的原始表(显然仅在测试时):-

    DROP TABLE IF EXISTS oldmyTable;

    您可能希望忽略此操作,直到您确定迁移已成功。

最终结果是表格应该是预期的


关于评论:-

问题是我有 16-20 个表要迁移。

你可以使用类似的东西:-

public static int preMigrateAdjustment(SQLiteDatabase mDB) 

    String original_rename_prefix = "old";
    String tempname_suffix = "temp";
    String newsql_column = "newsql";
    String[] columns = new String[]
            "name",
            "replace(replace(sql,' BOOL ',' INTEGER '),' BYTE ',' INTEGER ') AS " + newsql_column
    ;

    int count_done = 0;
    String whereclause = "name LIKE('" + 
            original_rename_prefix +
            "%') AND type = 'table'";
    Cursor csr = mDB.query("sqlite_master",null,whereclause,null,null,null,null);
    while (csr.moveToNext()) 
        mDB.execSQL("DROP TABLE IF EXISTS " + csr.getString(csr.getColumnIndex("name")));
    


    whereclause = "type = 'table' AND (instr(sql,' BOOL ')  OR instr(sql,' BYTE '))";
    csr = mDB.query(
            "sqlite_master",
            columns,
            whereclause,
            null,null,null,null
    );
    while (csr.moveToNext()) 
        String base_table_name = csr.getString(csr.getColumnIndex("name"));
        String newsql = csr.getString(csr.getColumnIndex(newsql_column));
        String temp_table_name = base_table_name + tempname_suffix;
        String renamed_table_name = original_rename_prefix+base_table_name;
        mDB.execSQL(newsql.replace(base_table_name,temp_table_name));
        mDB.execSQL("INSERT INTO " + temp_table_name + " SELECT * FROM " + base_table_name);
        mDB.execSQL("ALTER TABLE " + base_table_name + " RENAME TO " + renamed_table_name);
        mDB.execSQL("ALTER TABLE " + temp_table_name + " RENAME TO " + base_table_name);
        count_done++;
    
    whereclause = "name LIKE('" + 
            original_rename_prefix +
            "%') AND type = 'table'";
    csr = mDB.query("sqlite_master",null,whereclause,null,null,null,null);
    while (csr.moveToNext()) 
        mDB.execSQL("DROP TABLE IF EXISTS " + csr.getString(csr.getColumnIndex("name")));
    
    csr.close();
    return count_done;

请注意,这不是万无一失的,例如如果您碰巧有已经以 old 开头的表,那么这些表将被删除。 以上假设第二次运行实际删除重命名的原始表。

附加

在解决 BOOL BYTE 类型后,研究这个并实际测试(在本例中使用 5 个表)具有相同架构的另一个问题出现在该编码中

_id INTEGER PRIMARY KEY AUTOINCREMENT 

导致notNull = false,同时编码

@PrimaryKey(autoGenerate = true)
private long _id;

导致 notNull=true

例如假设 AUTOINCREMENT NOT NULL 未编码的快速修复 preMigrateAdjustment 中的行已从 :-

mDB.execSQL((newsql.replace(base_table_name,temp_table_name)));

到:-

mDB.execSQL((newsql.replace(base_table_name,temp_table_name)).replace("AUTOINCREMENT","AUTOINCREMENT NOT NULL"));

工作演示

创建和填充旧的(房间前)表。

在 Database Helper OrginalDBHelper.java 中创建和填充旧表:-

public class OriginalDBHelper extends SQLiteOpenHelper 

    public static final String DBNAME = "mydb";
    public static final int DBVERSION = 1;

    int tables_to_create = 5; //<<<<<<<<<< 5 sets of tables

    SQLiteDatabase mDB;

    public OriginalDBHelper(Context context) 
        super(context, DBNAME, null, DBVERSION);
        mDB = this.getWritableDatabase();
    

    @Override
    public void onCreate(SQLiteDatabase db) 

        for (int i=0;i < tables_to_create;i++) 

            db.execSQL("CREATE TABLE IF NOT EXISTS myTable" + String.valueOf(i) + "X (_id INTEGER PRIMARY KEY AUTOINCREMENT,\n" +
                    "            my_first_field BOOL NOT NULL DEFAULT 0,\n" +
                    "                    my_second_field BYTE NOT NULL DEFAULT 0)"
            );

            db.execSQL("INSERT INTO myTable" + String.valueOf(i) + "X (my_first_field,my_second_field) VALUES(0,0),(1,0),(1,1),(0,1)");
        
    

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) 

    

表的迁移前转换

即调整架构以适应房间)PreMigrationAdjustment.java

public class PreMigrationAdjustment 

    public static int preMigrateAdjustment(SQLiteDatabase mDB) 

        String original_rename_prefix = "old";
        String tempname_suffix = "temp";
        String newsql_column = "newsql";
        String[] columns = new String[]
                "name",
                "replace(replace(sql,' BOOL ',' INTEGER '),' BYTE ',' INTEGER ') AS " + newsql_column
        ;

        int count_done = 0;
        String whereclause = "name LIKE('" +
                original_rename_prefix +
                "%') AND type = 'table'";
        Cursor csr = mDB.query("sqlite_master",null,whereclause,null,null,null,null);
        while (csr.moveToNext()) 
            mDB.execSQL("DROP TABLE IF EXISTS " + csr.getString(csr.getColumnIndex("name")));
        


        whereclause = "type = 'table' AND (instr(sql,' BOOL ')  OR instr(sql,' BYTE '))";
        csr = mDB.query(
                "sqlite_master",
                columns,
                whereclause,
                null,null,null,null
        );
        while (csr.moveToNext()) 
            String base_table_name = csr.getString(csr.getColumnIndex("name"));
            String newsql = csr.getString(csr.getColumnIndex(newsql_column));
            String temp_table_name = base_table_name + tempname_suffix;
            String renamed_table_name = original_rename_prefix+base_table_name;
            mDB.execSQL((newsql.replace(base_table_name,temp_table_name)).replace("AUTOINCREMENT","AUTOINCREMENT NOT NULL"));
            //mDB.execSQL((newsql.replace(base_table_name,temp_table_name)));
            mDB.execSQL("INSERT INTO " + temp_table_name + " SELECT * FROM " + base_table_name);
            mDB.execSQL("ALTER TABLE " + base_table_name + " RENAME TO " + renamed_table_name);
            mDB.execSQL("ALTER TABLE " + temp_table_name + " RENAME TO " + base_table_name);
            count_done++;
        
        whereclause = "name LIKE('" +
                original_rename_prefix +
                "%') AND type = 'table'";
        csr = mDB.query("sqlite_master",null,whereclause,null,null,null,null);
        while (csr.moveToNext()) 
            mDB.execSQL("DROP TABLE IF EXISTS " + csr.getString(csr.getColumnIndex("name")));
        
        csr.close();
        return count_done;
    

警告这太简单了,如果不考虑它的缺陷就无法使用,并且仅用于演示。

房间的实体

为简洁起见,仅显示 5 个中的 1 个,即 myTable0X.java

显然,这些必须仔细编写以匹配前厅表。

@Entity()
public class myTable0X 

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "_id")
    private long id;

    @ColumnInfo(name = "my_first_field")
    private boolean my_first_field;
    @ColumnInfo(name = "my_second_field")
    private boolean my_second_field;

    public long getId() 
        return id;
    

    public void setId(long id) 
        this.id = id;
    

    public boolean isMy_first_field() 
        return my_first_field;
    

    public void setMy_first_field(boolean my_first_field) 
        this.my_first_field = my_first_field;
    

    public boolean isMy_second_field() 
        return my_second_field;
    

    public void setMy_second_field(boolean my_second_field) 
        this.my_second_field = my_second_field;
    

单个 DAO 接口 DAOmyTablex.java

@Dao
public interface DAOmyTablex 

    @Query("SELECT * FROM myTable0X")
    List<myTable0X> getAllFrommyTable0();

    @Query("SELECT * FROM myTable1X")
    List<myTable1X> getAllFrommyTable1();

    @Query("SELECT * FROM myTable2X")
    List<myTable2X> getAllFrommyTable2();

    @Query("SELECT * FROM myTable3X")
    List<myTable3X> getAllFrommyTable3();

    @Query("SELECT * FROM myTable4X")
    List<myTable4X> getAllFrommyTable4();

    @Insert
    long[] insertAll(myTable0X... myTable0XES);

    @Insert
    long[] insertAll(myTable1X... myTable1XES);

    @Insert
    long[] insertAll(myTable2X... myTable2XES);

    @Insert
    long[] insertAll(myTable3X... myTable3XES);

    @Insert
    long[] insertAll(myTable4X... myTable4XES);

    @Delete
    int delete(myTable0X mytable0X);

    @Delete
    int delete(myTable1X mytable1X);

    @Delete
    int delete(myTable2X mytable2X);

    @Delete
    int delete(myTable3X mytable3X);

    @Delete
    int delete(myTable4X mytable4X);


数据库 mydb.java

@Database(entities = myTable0X.class, myTable1X.class, myTable2X.class, myTable3X.class, myTable4X.class,version = 2)
public abstract class mydb extends RoomDatabase 
    public abstract DAOmyTablex dbDAO();

请注意,所有 5 个实体均已使用。 注意,由于当前数据库版本为1,room需要增加版本号,因此version = 2

把它们放在一起MainActivity.java

这包括 3 个核心阶段

    构建前房数据库。 将桌子改造成适合房间。 通过房间打开(移交)数据库。

当应用启动时,它会自动执行第 1 阶段和第 2 阶段,添加了一个按钮,点击该按钮后将执行第 3 阶段(仅一次)。

最后,从表中提取数据(这实际上打开了 Room 数据库) 并将其中一张表中的数据输出到日志中。

public class MainActivity extends AppCompatActivity 

    OriginalDBHelper mDBHlpr;
    Button mGo;
    mydb mMyDB;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mGo = this.findViewById(R.id.go);
        mGo.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View v) 
                goForIt();
            
        );

        mDBHlpr = new OriginalDBHelper(this);
        Log.d("STAGE1","The original tables");
        dumpAllTables();
        Log.d("STAGE2", "Initiaing pre-mirgration run.");
        Log.d("STAGE2 A RESULT",
                String.valueOf(
                        PreMigrationAdjustment.preMigrateAdjustment(mDBHlpr.getWritableDatabase()
                        )
                ) + " tables converted."
        ); //<<<<<<<<<< CONVERT THE TABLES
        Log.d("STAGE2 B","Dumping adjusted tables");
        dumpAllTables();
        Log.d("STAGE2 C","Second run Cleanup");
        Log.d("STAGE2 DRESULT",
                String.valueOf(
                        PreMigrationAdjustment.preMigrateAdjustment(mDBHlpr.getWritableDatabase()
                        )
                ) + " tables converted."
        ); //<<<<<<<<<< CONVERT THE TABLES
        dumpAllTables();
        Log.d("STAGE3","Handing over to ROOM (when button is clicked)");
    

    private void goForIt() 
        if (mMyDB != null) return;
        mMyDB = Room.databaseBuilder(this,mydb.class,OriginalDBHelper.DBNAME).addMigrations(MIGRATION_1_2).allowMainThreadQueries().build();
        List<myTable0X> mt0 = mMyDB.dbDAO().getAllFrommyTable0();
        List<myTable1X> mt1 = mMyDB.dbDAO().getAllFrommyTable1();
        List<myTable2X> mt2 = mMyDB.dbDAO().getAllFrommyTable2();
        List<myTable3X> mt3 = mMyDB.dbDAO().getAllFrommyTable3();
        List<myTable4X> mt4 = mMyDB.dbDAO().getAllFrommyTable4();
        for (myTable0X mt: mt0) 
            Log.d("THIS_MT","ID is " + String.valueOf(mt.getId()) + " FIELD1 is " + String.valueOf(mt.isMy_first_field()) + " FIELD2 is " + String.valueOf(mt.isMy_second_field()));
        
        // etc.......
    

    private void dumpAllTables() 
        SQLiteDatabase db = mDBHlpr.getWritableDatabase();
        Cursor c1 = db.query("sqlite_master",null,"type = 'table'",null,null,null,null);
        while (c1.moveToNext()) 
            Log.d("TABLEINFO","Dmuping Data for Table " + c1.getString(c1.getColumnIndex("name")));
            Cursor c2 = db.query(c1.getString(c1.getColumnIndex("name")),null,null,null,null,null,null);
            DatabaseUtils.dumpCursor(c2);
            c2.close();
        
        c1.close();
    

    public final Migration MIGRATION_1_2 = new Migration(1, 2) 
        @Override
        public void migrate(SupportSQLiteDatabase database) 
            /**NOTES
            //Tried the pre-migration here BUT SQLiteDatabaseLockedException: database is locked (code 5 SQLITE_BUSY)
            //Cannot use SupportSQLiteDatabase as that locks out access to sqlite_master
            //PreMigrationAdjustment.preMigrateAdjustment(mDBHlpr.getWritableDatabase()); //Initial run
            //PreMigrationAdjustment.preMigrateAdjustment(mDBHlpr.getWritableDatabase()); //Cleanup run
            */
        
    ;

由于 room 会考虑正在进行迁移,因此 Migration 对象的迁移方法被不执行任何操作的方法覆盖。 根据 cmets 尝试利用迁移,问题是数据库被房间锁定,并且传递给 migration 方法的 SupportSQliteDatabase 没有'不允许访问 sqlite_master

结果

结果(只是 STAGE???? 输出)是:-

2019-05-19 13:18:12.227 D/STAGE1: The original tables
2019-05-19 13:18:12.244 D/STAGE2: Initiaing pre-mirgration run.
2019-05-19 13:18:12.281 D/STAGE2 A RESULT: 5 tables converted.
2019-05-19 13:18:12.281 D/STAGE2 B: Dumping adjusted tables
2019-05-19 13:18:12.303 D/STAGE2 C: Second run Cleanup
2019-05-19 13:18:12.304 D/STAGE2 DRESULT: 0 tables converted.
2019-05-19 13:18:12.331 D/STAGE3: Handing over to ROOM (when button is clicked)

决赛排是:-

2019-05-19 13:20:03.090 D/THIS_MT: ID is 1 FIELD1 is false FIELD2 is false
2019-05-19 13:20:03.090 D/THIS_MT: ID is 2 FIELD1 is true FIELD2 is false
2019-05-19 13:20:03.090 D/THIS_MT: ID is 3 FIELD1 is true FIELD2 is true
2019-05-19 13:20:03.090 D/THIS_MT: ID is 4 FIELD1 is false FIELD2 is true

【讨论】:

这是我管理它的实际方式,因为除了迁移它似乎别无他法。问题是数据库已经创建并填充。问题是我要迁移 16-20 个表。很多。如果没有更好的答案,或者有什么直接的方法,我会投票并选择它作为正确答案。 @AndresOller 您可以使用SELECT name, replace(replace(sql,' BOOL ',' INTEGER '),' BYTE ',' INTEGER ') AS newsql FROM sqlite_master WHERE instr(sql,' BOOL ') OR instr(sql,' BYTE '); 之类的东西来驱动多表进程。所以对于每一行(如果没有,什么也不做)你有表名和调整后的 SQL。

以上是关于Can't migrate a table to Room do to an error with the way booleans is saved in Sqlite的主要内容,如果未能解决你的问题,请参考以下文章

“Unable to create the django_migrations table (%s)

报错 raise MigrationSchemaMissing("Unable to create the django_migrations table (%s)" % exc)

Laravel migrate:回滚添加和删除表列

Laravel migration 在进行很小的修改时怎么解决

Yii 2 migration 给表添加字段

Python 3.8 TypeError: can't concat str to bytes - TypeError: a bytes-like object is required, not 's