Не удается перенести таблицу в Room из-за ошибки, связанной со способом сохранения логических значений в Sqlite

Я пытался перенести свое приложение на Room. Я борюсь с конкретной таблицей, которую нельзя перенести напрямую из-за того, как она была создана.

Поля были созданы с типом данных BOOL и BYTE вместо INTEGER.

Я уже не пытался:

  • Измените поля моей сущности на Int/Boolean/Byte с той же ошибкой
  • Создание TypeConverter для сохранения его как Boolean/Byte
  • Добавление typeAffinity как UNDEFINED в @ColumnInfo моей сущности, которая имеет сходство = 1

Мое 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:
TableInfo{name='my_table', columns = {_id=Column{name='_id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1}, my_first_field=Column{name='my_first_field', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0}, my_second_field=Column{name='my_second_field', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0}}, foreignKeys=[], indices=[]}
     Found:
TableInfo{name='my_table', columns = {_id=Column{name='_id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1}, my_first_field=Column{name='my_first_field', type='BOOL', affinity='1', notNull=true, primaryKeyPosition=0}, my_second_field=Column{name='my_second_field', type='BYTE', affinity='1', notNull=true, primaryKeyPosition=0}}, foreignKeys=[], indices=[]}

Есть ли способ сделать прямо вперед, не создавая стратегию миграции?

добавьте больше кода, пожалуйста!

mohammadReza Abiri 18.05.2019 00:18

@mohammadRezaAbiri, какой еще код вы считаете полезным/необходимым?

Andres Oller 18.05.2019 00:44

Sqlite не имеет типов «логический» или «байтовый»… Используйте целое число. sqlite.org/datatype3.html

Shawn 18.05.2019 01:04

Я знаю, что SQLITE не имеет логического или байтового значения. На самом деле, если база данных новая, то этой ошибки не происходит, потому что поле базы данных инициализируется как INTEGER, а не как BYTE или BOOL. Мой вопрос в том, как я могу избежать уничтожения своих таблиц и скопировать информацию в новую таблицу.

Andres Oller 18.05.2019 01:34
4
4
1 213
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я думаю, вы могли бы, до, создающий базу данных комнат: -

  1. Проверьте, не нужно ли что-то сделать, например. используя :-

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

    • а потом проверяем результат.

    • Если он равен 0, больше ничего не делайте (хотя на всякий случай вы можете использовать DROP TABLE IF EXISTS oldmyTable только тогда, когда он равен 0).

    • ТОЛЬКО Если приведенное выше возвращает 1, то: -

  2. удалите переименованную исходную таблицу (см. ниже, а также выше) на всякий случай, если она существует: -

    • DROP TABLE IF EXISTS oldmyTable;
  3. определить другую таблицу, используя

    • 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)

    • то есть схема ожидал

  4. заполнить новую таблицу, используя

    • INSERT INTO myOtherTable SELECT * FROM myTable;
  5. переименуйте mytable, используя: -

    • ALTER TABLE mytable RENAME TO oldmyTable;
  6. переименуйте myOtherTable, используя исходное имя: -

    • ALTER TABLE myOtherTable RENAME TO mytable;
  7. удалить переименованную исходную таблицу (очевидно, только при проверке): -

    • DROP TABLE IF EXISTS oldmyTable;

      • Вы можете пропустить это, пока не будете уверены, что миграция сработала.

Конечным результатом является то, что таблица должна быть такой, как ожидал.


Что касается комментария: -

Problem is that I have like 16-20 tables to migrate.

Вы можете использовать что-то вроде: -

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, то они будут удалены.
  • Вышеприведенное предполагает второй запуск, чтобы фактически удалить переименованные исходные таблицы.

Дополнительный

Глядя на это и фактически тестируя (в данном случае с использованием 5 таблиц) с идентичной схемой после разрешения типов BOOL BYTE, в этом кодировании выявляется дополнительная проблема.

_id INTEGER PRIMARY KEY AUTOINCREMENT 

приводит к ненуль = ложь, в то время как кодирование

@PrimaryKey(autoGenerate = true)
private long _id;

приводит к ненуль = истина

Например, быстрое исправление, которое предполагает, что 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"));

Рабочая демонстрация

Создание и заполнение старых (pre-room) таблиц.

Создание и заполнение старых таблиц выполняется в помощнике по базе данных Исходный DBHelper.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;
    }
}
  • ВНИМАНИЕ, это слишком просто, чтобы использовать его без учета его недостатков, и оно предназначено только для демонстрации.

Сущности для комнаты

только 1 из 5 показанных для краткости, т.е. 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, для комнаты требуется увеличить номер версии, поэтому версия = 2

Собираем все вместе MainActivity.java

Он состоит из 3 основных этапов

  1. Создание базы данных pre-room.
  2. Преобразование столов в соответствии с комнатой.
  3. Открытие (передача) базы через room.

Когда приложение запускается, оно автоматически выполняет этапы 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
            */
        }
    };
}
  • Поскольку комната будет считать, что миграция выполняется, объект Migration имеет метод миграции, переопределенный методом, который ничего не делает.
  • Согласно комментариям, были предприняты попытки использовать миграцию, проблема в том, что база данных заблокирована по комнате и что База данных SupportSQlite, переданный методу миграция, не разрешает доступ к 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 таблиц. МНОГО. Если нет лучшего ответа или какого-либо способа сделать это прямо, я проголосую за это и выберу его как правильный ответ.

Andres Oller 18.05.2019 01:36

@AndresOller Вы можете использовать что-то вроде SELECT name, replace(replace(sql,' BOOL ',' INTEGER '),' BYTE ',' INTEGER ') AS newsql FROM sqlite_master WHERE instr(sql,' BOOL ') OR instr(sql,' BYTE '); для управления процессом с несколькими таблицами. Итак, для каждой строки (ничего не делайте, если нет) у вас есть имя таблицы и скорректированный SQL.

MikeT 18.05.2019 02:01

Другие вопросы по теме