Создание комнаты из актива + fallbackToDestructiveMigration, воссоздание базы данных при каждом запросе

Используя Room, я загружаю свою базу данных из ресурсов (уже заполненная база данных).

Я планирую очень часто обновлять содержимое базы данных, поэтому каждый раз, когда версия увеличивается, старая база данных, которую пользователь установил в своем хранилище, должна быть удалена и сгенерирована снова (простым копированием новой базы данных в хранилище).

Предполагается, что этот процесс будет выполняться автоматически комнатой при включении fallbackToDestructiveMigration(). Однако я обнаружил, что при включении fallbackToDestructiveMigration() вместе с createFromAsset() каждый раз, когда запрашивается база данных, Room удаляет старую базу данных, даже если ее версия уже является последней, и создает ее снова, как если бы потребовалась миграция.

@Module
@InstallIn(SingletonComponent.class)
public class TestRoomModule {

    private static final String DATABASE_NAME = "test.db";

    @Provides
    public TestDatabase provideDatabase(@ApplicationContext Context context) {
        return Room.databaseBuilder(
                        context.getApplicationContext(),
                        TestDatabase.class,
                        DATABASE_NAME)
                .createFromAsset("databases/test.db")
                .fallbackToDestructiveMigration()
                .allowMainThreadQueries()
                .build();
    }
}

Нравится: эта проблема упоминается здесь

Решение, предложенное в предыдущем сообщении, просто удаляет «fallbackToDestructiveMigration», но это не решение, а обходной путь, поскольку если я обновлю свою базу данных и версию моей базы данных, приложение выдаст исключение, поскольку миграция не была предоставлена.

@Database(entities = {
    version = 1)
public abstract class TestDatabase extends RoomDatabase {

Обновление до версии 2:

@Database(entities = {
    version = 2)
public abstract class TestDatabase extends RoomDatabase {

Выкинет: A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase

Как я могу это исправить, чтобы fallbackToDestructiveMigration() срабатывал ТОЛЬКО тогда, когда версия базы данных действительно изменилась?

Можете ли вы отредактировать свой вопрос, указав пример версий до и после (т. е. какие изменения, поскольку моя база данных может быть активом, это может быть существующая база данных или даже и то, и другое). Вы также можете просмотреть вопросы и ответы под тегом android-room-prepackageddatabase.

MikeT 27.04.2023 12:27

Итак, вы используете createFromAsset(); это означает, что ваша база данных заполняется в активах, теперь какие именно изменения в базе данных вы делаете (это изменение строк? или изменение схемы БД, например добавление/удаление столбцов?). Это изменение со стороны разработчика или пользователя? Я думаю, что это сделано разработчиком и помещено в следующий выпуск приложения, я правильно понимаю?

Zain 26.05.2023 22:23

@Zain Да, сделанные модификации могут быть как строками, так и структурой данных, и они могут быть выполнены только разработчиком.

Nexussim Lements 28.05.2023 00:43
1
3
123
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Решение, предложенное предыдущим постом, просто удалить «fallbackToDestructiveMigration», но это не решение, а обходной путь, поскольку если я обновлю свою базу данных и версию моей базы данных, приложение выдаст исключение, поскольку миграция не была предоставлена.

Удаление fallbackToDestructiveMigration решит проблему перезаписи/воссоздания базы данных при каждом запросе.

Но теперь вы не сможете выполнить миграцию базы данных в следующем выпуске, потому что вы не укажете план миграции с помощью addMigrations(), иначе вы получите IllegalStateException.

Попытка поймать это исключение потерпит неудачу, поскольку Room этого не допустит, например:

@Provides
public TestDatabase provideDatabase(@ApplicationContext Context context) {
    
    TestDatabase database;
    
    try {
        database = Room.databaseBuilder(
                    context.getApplicationContext(),
                    TestDatabase.class,
                    DATABASE_NAME)
            .createFromAsset("databases/test.db")
            .allowMainThreadQueries()
            .build();

    } catch (IllegalStateException e) {
        database = Room.databaseBuilder(
                    context.getApplicationContext(),
                    TestDatabase.class,
                    DATABASE_NAME)
            .createFromAsset("databases/test.db")
            .fallbackToDestructiveMigration()
            .allowMainThreadQueries()
            .build();
    }
    
    return database;

}

Итак, поскольку у вас нет планов миграции с addMigrations(), единственный способ - манипулировать с помощью fallbackToDestructiveMigration каким-либо флагом, поэтому мы вызываем fallbackToDestructiveMigration условно по необходимости.

Возможное решение состоит в том, чтобы управлять двумя целыми числами, одно из которых указывает на текущую версию базы данных, а другое — на новую версию базы данных. Либо SharedPrefs, либо DataStore являются типичными инструментами для этого:

Предполагая, что текущая версия базы данных равна 1, а в новой версии — 2:

Создайте SharedPrefs вспомогательные методы:

static String SHARED_PREFS_NAME = "SHARED_PREFS_NAME";
static String CURRENT_DB_VERSION = "CURRENT_DB_VERSION";
static String NEW_DB_VERSION = "NEW_DB_VERSION";

public static int getCurrentDBVersion(Context context) {
    SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
    return prefs.getInt(CURRENT_DB_VERSION , 1);
}

public static void setCurrentDBVersion(Context context, int version) {
    SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
    SharedPreferences.Editor editor = prefs.edit();
    editor.putInt(CURRENT_DB_VERSION , version);
    editor.apply();
}

public static int getNewDBVersion(Context context) {
    SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
    return prefs.getInt(NEW_DB_VERSION, 1);
}

public static void setNewDBVersion(Context context, int version) {
    SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
    SharedPreferences.Editor editor = prefs.edit();
    editor.putInt(NEW_DB_VERSION, version);
    editor.apply();
}

Затем проверьте условие в классе базы данных:

@Database(entities = {...},
    version = 2)
public abstract class TestDatabase extends RoomDatabase {

    @Provides
    public TestDatabase provideDatabase(@ApplicationContext Context context) {

        TestDatabase database;

        int newDBVersion = getNewDBVersion(context);
        int currentDBVersion = getNewDBVersion(context);

        if (newDBVersion == currentDBVersion) { // No migration required
            database = Room.databaseBuilder(
                        context.getApplicationContext(),
                        TestDatabase.class,
                        DATABASE_NAME)
                .createFromAsset("databases/test.db")
                .allowMainThreadQueries()
                .build();
                
        } else { // Migration is needed (call fallbackToDestructiveMigration)
            database = Room.databaseBuilder(
                        context.getApplicationContext(),
                        TestDatabase.class,
                        DATABASE_NAME)
                .createFromAsset("databases/test.db")
                .fallbackToDestructiveMigration()
                .allowMainThreadQueries()
                .build();
            
            // set the current version to the new version as the migration is done.
            setCurrentDBVersion(context, newDBVersion);

        }

        return database;
    }
}   

И всякий раз, когда у вас есть выпуск, требующий миграции базы данных, вы должны установить новую версию базы данных на новую версию, это будет в обратном вызове активности onCreate():

 setNewDBVersion(this, 2);

Обязательно установите это перед доступом к базе данных.

Если в какой-то версии миграция не требуется, оставьте setNewDBVersion(this, 1) текущую версию.


Другой подход к условному вызову fallbackToDestructiveMigration()

Вместо того, чтобы хранить версии базы данных в каком-то хранилище (SharedPrefs/DataStore), мы бы проверили версию базы данных:

Цитата из этого ответа

Измененную (увеличенную) USER_VERSION можно обнаружить, просто обратившись файлы (текущая база данных и актив) и чтение первого 100 байт, а затем извлечь 4 байта по смещению 60 (68 для идентификатор приложения).

И этот ответ для этой цели ссылается на приведенный ниже метод:

private static boolean isNewAsset(Context context, String asset, String dbname) {
    File current_Db = context.getDatabasePath(dbname);
    if (!current_Db.exists()) return false; /* No Database then nothing to do */
    int current_Db_version = getDBVersion(current_Db);
    Log.d("DBINFO","isNewAsset has determined that the current database version is " + current_Db_version);
    if (current_Db_version < 0) return false; /* No valid version */
    int asssetVersion = getAssetVersion(context,asset);
    Log.d("DBINFO","isNewAsset has determined that the asset version is " + asssetVersion);
    if (asssetVersion > current_Db_version) {
        Log.d("DBINFO","isNewAsset has found that the asset version is greater than the current db version " + current_Db_version);
        return true;
    } else {
        Log.d("DBINFO","isNewAsset has found that the asset version is unchanged " + current_Db_version);
    }
    return false;
}

Боковые примечания:

  • allowMainThreadQueries() не следует использовать в рабочем приложении, его можно использовать только для тестирования и простоты.

  • Лично я бы предпочел добавить миграцию базы данных вместо использования fallbackToDestructiveMigration() даже для предварительно заполненных баз данных; вероятно, поскольку последнее разрушительно :).

Если нет изменений схемы (таблиц/столбцов); т. е. просто нужно обновить записи базы данных (строки); мы можем просто иметь пустой план миграции и просто удалять базу данных при любом обновлении с помощью deleteDatabase() в контексте, чтобы обновить базу данных в активах:

@Database(entities = {...},
    version = 2)
public abstract class TestDatabase extends RoomDatabase {

    @Provides
    public TestDatabase provideDatabase(@ApplicationContext Context context) {

        int newDBVersion = getNewDBVersion(context);
        int currentDBVersion = getNewDBVersion(context);

        if (newDBVersion != currentDBVersion) { // Migration required
            
            // delete the database to refresh it on the assets and remove any cached version
            context.deleteDatabase("databases/test.db");
            
            // set the current version to the new version as the migration is done.
            setCurrentDBVersion(context, newDBVersion);
        }
        
        return Room.databaseBuilder(
                        context.getApplicationContext(),
                        TestDatabase.class,
                        DATABASE_NAME)
                .createFromAsset("databases/test.db")
                .allowMainThreadQueries() 
                .addMigrations(migration_v1_v2)
                .build();
    }

    // Empty Migration plan
    static Migration migration_v1_v2 = new Migration(1, 2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            Log.d("TAG","Empty Migration plan for data insertion, deletion, or update - Migration from V1 to V2");
        }
    };
}

Такой подробный и хороший ответ сработал как шарм!

Nexussim Lements 31.05.2023 09:00

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