Миграция createFromAsset, но не меняйте столбцы c - PullRequest
1 голос
/ 07 января 2020

У меня есть своего рода приложение для викторины, и у меня есть база данных со всеми вопросами в таблицах, для каждого вопроса есть столбец solved, который я обновляю, если ответ правильный, поэтому я могу фильтровать с помощью SQL WHERE, чтобы показать только нерешенные вопросы. Теперь время от времени я должен исправлять опечатки в вопросах или, возможно, захочу добавить некоторые новые, поэтому

Как использовать исправленную базу данных (questions.db) из ресурсов в сохранил один на пользовательском устройстве, сохраняя solved столбцы ?

Я думал и попробовал следующие вещи без успеха:

  • В настоящее время я использую Самодельное решение для замены базы данных на устройстве (деструктивное), но между обновлениями сохраняйте решенную информацию https://github.com/ueen/RoomAsset

  • Поместите решенную информацию (идентификатор вопроса решен) да / нет) в отдельной таблице и LEFT JOIN для фильтрации нерешенных вопросов, это только сложные вопросы

  • Есть дополнительная база данных для решенных вопросов, кажется, нет простого способа прикрепить две базы данных Room

Итак, по сути, это может быть источником вдохновения для команды разработчиков комнаты, я хотел бы иметь правильную стратегию миграции для createFromAsset с возможностью указывать определенные столбцы Умнс / таблицы должны быть сохранены. Спасибо за вашу великолепную работу, мне действительно нравится Android Jetpack и особенно Room! Кроме того, я рад любому обходному решению, которое я мог бы использовать для решения этой проблемы:)

Ответы [ 2 ]

1 голос
/ 08 января 2020

Я полагаю, что следующее делает то, что вы хотите

@Database(version = DatabaseConstants.DBVERSION, entities = {Question.class})
public abstract class QuestionDatabase extends RoomDatabase {

    static final String DBNAME = DatabaseConstants.DBNAME;

    abstract QuestionDao questionsDao();

    public static QuestionDatabase getInstance(Context context) {
        copyFromAssets(context,false);
        if (getDBVersion(context,DatabaseConstants.DBNAME) < DatabaseConstants.DBVERSION) {
            copyFromAssets(context,true);
        }
        return Room.databaseBuilder(context,QuestionDatabase.class,DBNAME)
                .addCallback(callback)
                .allowMainThreadQueries()
                .addMigrations(Migration_1_2)
                .build();
    }

    private static RoomDatabase.Callback callback = new Callback() {
        @Override
        public void onCreate(@NonNull SupportSQLiteDatabase db) {
            super.onCreate(db);
        }

        @Override
        public void onOpen(@NonNull SupportSQLiteDatabase db) {
            super.onOpen(db);
        }

        @Override
        public void onDestructiveMigration(@NonNull SupportSQLiteDatabase db) {
            super.onDestructiveMigration(db);
        }
    };

    private static Migration Migration_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
        }
    };

    private static boolean doesDatabaseExist(Context context) {
        if (new File(context.getDatabasePath(DBNAME).getPath()).exists()) return true;
        if (!(new File(context.getDatabasePath(DBNAME).getPath()).getParentFile()).exists()) {
            new File(context.getDatabasePath(DBNAME).getPath()).getParentFile().mkdirs();
        }
        return false;
    }

    private static void copyFromAssets(Context context, boolean replaceExisting) {
        boolean dbExists = doesDatabaseExist(context);
        if (dbExists && !replaceExisting) return;
        //First Copy
        if (!replaceExisting) {
            copyAssetFile(context);
            return;
        }
        //Subsequent Copies

        File originalDBPath = new File(context.getDatabasePath(DBNAME).getPath());
        // Open and close the original DB so as to checkpoint the WAL file
        SQLiteDatabase originalDB = SQLiteDatabase.openDatabase(originalDBPath.getPath(),null,SQLiteDatabase.OPEN_READWRITE);
        originalDB.close();

        //1. Rename original database
        String preservedDBName = "preserved_" + DBNAME;
        File preservedDBPath = new File (originalDBPath.getParentFile().getPath() + preservedDBName);
        (new File(context.getDatabasePath(DBNAME).getPath()))
                .renameTo(preservedDBPath);

        //2. Copy the replacement database from the assets folder
        copyAssetFile(context);

        //3. Open the newly copied database
        SQLiteDatabase copiedDB = SQLiteDatabase.openDatabase(originalDBPath.getPath(),null,SQLiteDatabase.OPEN_READWRITE);
        SQLiteDatabase preservedDB = SQLiteDatabase.openDatabase(preservedDBPath.getPath(),null,SQLiteDatabase.OPEN_READONLY);

        //4. get the orignal data to be preserved
        Cursor csr = preservedDB.query(
                DatabaseConstants.QUESTION_TABLENAME,DatabaseConstants.EXTRACT_COLUMNS,
                null,null,null,null,null
        );

        //5. Apply preserved data to the newly copied data
        copiedDB.beginTransaction();
        ContentValues cv = new ContentValues();
        while (csr.moveToNext()) {
            cv.clear();
            for (String s: DatabaseConstants.PRESERVED_COLUMNS) {
                switch (csr.getType(csr.getColumnIndex(s))) {
                    case Cursor.FIELD_TYPE_INTEGER:
                        cv.put(s,csr.getLong(csr.getColumnIndex(s)));
                        break;
                    case Cursor.FIELD_TYPE_STRING:
                        cv.put(s,csr.getString(csr.getColumnIndex(s)));
                        break;
                    case Cursor.FIELD_TYPE_FLOAT:
                        cv.put(s,csr.getDouble(csr.getColumnIndex(s)));
                        break;
                    case Cursor.FIELD_TYPE_BLOB:
                        cv.put(s,csr.getBlob(csr.getColumnIndex(s)));
                        break;
                }
            }
            copiedDB.update(
                    DatabaseConstants.QUESTION_TABLENAME,
                    cv,
                    DatabaseConstants.QUESTION_ID_COLUMN + "=?",
                    new String[]{
                            String.valueOf(
                                    csr.getLong(
                                            csr.getColumnIndex(DatabaseConstants.QUESTION_ID_COLUMN
                                            )
                                    )
                            )
                    }
                    );
        }
        copiedDB.setTransactionSuccessful();
        copiedDB.endTransaction();
        csr.close();
        //6. Cleanup
        copiedDB.close();
        preservedDB.close();
        preservedDBPath.delete();
    }

    private static void copyAssetFile(Context context) {
        int buffer_size = 8192;
        byte[] buffer = new byte[buffer_size];
        int bytes_read = 0;
        try {
            InputStream fis = context.getAssets().open(DBNAME);
            OutputStream os = new FileOutputStream(new File(context.getDatabasePath(DBNAME).getPath()));
            while ((bytes_read = fis.read(buffer)) > 0) {
                os.write(buffer,0,bytes_read);
            }
            os.flush();
            os.close();
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("Unable to copy from assets");
        }
    }

    private static int getDBVersion(Context context, String databaseName) {
        SQLiteDatabase db = SQLiteDatabase.openDatabase( context.getDatabasePath(databaseName).getPath(),null,SQLiteDatabase.OPEN_READONLY);
        int rv = db.getVersion();
        db.close();
        return rv;
    }
}

Это управляет копией файла активов (в данном случае непосредственно из папки ресурсов) за пределами Room и до того, как база данных построена, делая свою собственную версию и проверка существования базы данных. Хотя ATTACH можно было бы использовать, решение сохраняет оригиналы и новые базы данных отдельно при обновлении новых с помощью курсора.

Была добавлена ​​некоторая гибкость / адаптивность в том смысле, что сохраняемые столбцы могут быть расширены. В тестовых прогонах DatabaseConstants включает в себя: -

public static final String[] PRESERVED_COLUMNS = new String[]
        {
                QUESTION_SOLVED_COLUMN
        };
public static final String[] EXTRACT_COLUMNS = new String[]
        {
                QUESTION_ID_COLUMN,
                QUESTION_SOLVED_COLUMN
        };

, поэтому могут быть добавлены дополнительные столбцы для сохранения (любого типа согласно 5. в методе copyFromAssets ). Извлекаемые столбцы также могут быть указаны, в приведенном выше случае столбец ID однозначно идентифицирует вопрос, так что он извлекается в дополнение к решенному столбцу для использования предложением WHERE.

Тестирование

Выше был проверен на: -

Оригинал

  • Скопируйте первую версию базы данных из активов, когда DBVERSION равен 1.

    • Обратите внимание, что этот оригинал содержит 3 вопроса согласно

    • enter image description here

    • Часть кода ( в вызывающей операции проверяется, чтобы видеть, все ли решенные значения равны 0, если это так, то он изменяет статус решенного вопроса с идентификатором 2)
  • Не копировать базу данных, но используйте существующую базу данных, когда DBVERSION равен 1 в последующих прогонах.
    • Идентификатор 2. остается решенным.

Новый

  • После переименования исходного актива из префикса в исходный_ изменив базу данных так, как показано ниже, и после копирования ее в файл ресурсов: -

    • enter image description here
  • Без изменения запуска DBVERSION (по-прежнему 1) и исходная база данных по-прежнему используется.

  • После изменения значения DBVERSION на 2 запуска копирует измененный файл ресурсов и восстанавливает / сохраняет решенный статус.

  • Для последующих запусков сохраненный статус для новых данных сохраняется.

Для тестирования активация вызова состояла из: -

public class MainActivity extends AppCompatActivity {

    QuestionDatabase questionDatabase;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        questionDatabase = QuestionDatabase.getInstance(this);
        int solvedCount = 0;
        for (Question q: questionDatabase.questionsDao().getAll()) {
            if (q.isSolved()) solvedCount++;
            q.logQuestion();
        }
        if (solvedCount == 0) {
            questionDatabase.questionsDao().setSolved(true,2);
        }
        for (Question q: questionDatabase.questionsDao().getAll()) {
            q.logQuestion();
        }
    }
} 

За каждый прогон выводит все вопросы в журнал дважды. После первого, если нет решенных вопросов, он решает вопрос с идентификатором 2.

Результат последнего запуска был: -

2020-01-08 09:14:37.689 D/QUESTIONINFO: ID is 1 Question is Editted What is x
      Answers Are :-
          a
          b
          x

    Correct Answer is 3

     Is Solved false
2020-01-08 09:14:37.689 D/QUESTIONINFO: ID is 2 Question is Edited What is a
      Answers Are :-
          a
          b
          c

    Correct Answer is 1

     Is Solved false
2020-01-08 09:14:37.689 D/QUESTIONINFO: ID is 3 Question is Edited What is b
      Answers Are :-
          a
          b
          c

    Correct Answer is 2

     Is Solved false
2020-01-08 09:14:37.689 D/QUESTIONINFO: ID is 4 Question is New Question What is d
      Answers Are :-
          e
          f
          d

    Correct Answer is 3

     Is Solved false
2020-01-08 09:14:37.692 D/QUESTIONINFO: ID is 1 Question is Editted What is x
      Answers Are :-
          a
          b
          x

    Correct Answer is 3

     Is Solved false
2020-01-08 09:14:37.692 D/QUESTIONINFO: ID is 2 Question is Edited What is a
      Answers Are :-
          a
          b
          c

    Correct Answer is 1

     Is Solved true
2020-01-08 09:14:37.692 D/QUESTIONINFO: ID is 3 Question is Edited What is b
      Answers Are :-
          a
          b
          c

    Correct Answer is 2

     Is Solved false
2020-01-08 09:14:37.693 D/QUESTIONINFO: ID is 4 Question is New Question What is d
      Answers Are :-
          e
          f
          d

    Correct Answer is 3

     Is Solved false

Дополнительно - Улучшенная версия

Это утвержденная версия, которая обслуживает несколько таблиц и столбцов. Для обслуживания таблиц был добавлен класс TablePreserve , который позволяет сохранить таблицу, столбцы, столбцы для извлечения и столбцы для предложения where. Согласно: -

public class TablePreserve {
    String tableName;
    String[] preserveColumns;
    String[] extractColumns;
    String[] whereColumns;

    public TablePreserve(String table, String[] preserveColumns, String[] extractColumns, String[] whereColumns) {
        this.tableName = table;
        this.preserveColumns = preserveColumns;
        this.extractColumns = extractColumns;
        this.whereColumns = whereColumns;
    }

    public String getTableName() {
        return tableName;
    }

    public String[] getPreserveColumns() {
        return preserveColumns;
    }

    public String[] getExtractColumns() {
        return extractColumns;
    }

    public String[] getWhereColumns() {
        return whereColumns;
    }
}

Вы создаете массив объектов TablePreserve, и они проходят через, например,

public final class DatabaseConstants {
    public static final String DBNAME = "question.db";
    public static final int DBVERSION = 2;
    public static final String QUESTION_TABLENAME = "question";
    public static final String QUESTION_ID_COLUMN = "id";
    public static final String QUESTION_QUESTION_COLUMN = QUESTION_TABLENAME;
    public static final String QUESTION_ANSWER1_COLUMN = "answer1";
    public static final String QUESTION_ANSWER2_COLUMN = "answer2";
    public static final String QUESTION_ANSWER3_COLUMN = "answer3";
    public static final String QUESTION_CORRECTANSWER_COLUMN = "correctAsnwer";
    public static final String QUESTION_SOLVED_COLUMN = "solved";

    public static final TablePreserve questionTablePreserve = new TablePreserve(
            QUESTION_TABLENAME,
            new String[]{QUESTION_SOLVED_COLUMN},
            new String[]{QUESTION_ID_COLUMN,QUESTION_SOLVED_COLUMN},
            new String[]{QUESTION_ID_COLUMN}
    );

    public static final TablePreserve[] TABLE_PRESERVELIST = new TablePreserve[] {
            questionTablePreserve
    };
}

Тогда База данных вопросов становится: -

@Database(version = DatabaseConstants.DBVERSION, entities = {Question.class})
public abstract class QuestionDatabase extends RoomDatabase {

    static final String DBNAME = DatabaseConstants.DBNAME;

    abstract QuestionDao questionsDao();

    public static QuestionDatabase getInstance(Context context) {
        if (!doesDatabaseExist(context)) {
            copyFromAssets(context,false);
        }
        if (getDBVersion(context, DatabaseConstants.DBNAME) < DatabaseConstants.DBVERSION) {
            copyFromAssets(context, true);
        }

        return Room.databaseBuilder(context,QuestionDatabase.class,DBNAME)
                .addCallback(callback)
                .allowMainThreadQueries()
                .addMigrations(Migration_1_2)
                .build();
    }

    private static RoomDatabase.Callback callback = new Callback() {
        @Override
        public void onCreate(@NonNull SupportSQLiteDatabase db) {
            super.onCreate(db);
        }

        @Override
        public void onOpen(@NonNull SupportSQLiteDatabase db) {
            super.onOpen(db);
        }

        @Override
        public void onDestructiveMigration(@NonNull SupportSQLiteDatabase db) {
            super.onDestructiveMigration(db);
        }
    };

    private static Migration Migration_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
        }
    };

    private static boolean doesDatabaseExist(Context context) {
        if (new File(context.getDatabasePath(DBNAME).getPath()).exists()) return true;
        if (!(new File(context.getDatabasePath(DBNAME).getPath()).getParentFile()).exists()) {
            new File(context.getDatabasePath(DBNAME).getPath()).getParentFile().mkdirs();
        }
        return false;
    }

    private static void copyFromAssets(Context context, boolean replaceExisting) {
        boolean dbExists = doesDatabaseExist(context);
        if (dbExists && !replaceExisting) return;
        //First Copy
        if (!replaceExisting) {
            copyAssetFile(context);
            setDBVersion(context,DBNAME,DatabaseConstants.DBVERSION);
            return;
        }
        //Subsequent Copies

        File originalDBPath = new File(context.getDatabasePath(DBNAME).getPath());
        // Open and close the original DB so as to checkpoint the WAL file
        SQLiteDatabase originalDB = SQLiteDatabase.openDatabase(originalDBPath.getPath(),null,SQLiteDatabase.OPEN_READWRITE);
        originalDB.close();

        //1. Rename original database
        String preservedDBName = "preserved_" + DBNAME;
        File preservedDBPath = new File (originalDBPath.getParentFile().getPath() + File.separator + preservedDBName);
        (new File(context.getDatabasePath(DBNAME).getPath()))
                .renameTo(preservedDBPath);

        //2. Copy the replacement database from the assets folder
        copyAssetFile(context);

        //3. Open the newly copied database
        SQLiteDatabase copiedDB = SQLiteDatabase.openDatabase(originalDBPath.getPath(),null,SQLiteDatabase.OPEN_READWRITE);
        SQLiteDatabase preservedDB = SQLiteDatabase.openDatabase(preservedDBPath.getPath(),null,SQLiteDatabase.OPEN_READONLY);

        //4. Apply preserved data to the newly copied data
        copiedDB.beginTransaction();
        for (TablePreserve tp: DatabaseConstants.TABLE_PRESERVELIST) {
            preserveTableColumns(
                    preservedDB,
                    copiedDB,
                    tp.getTableName(),
                    tp.getPreserveColumns(),
                    tp.getExtractColumns(),
                    tp.getWhereColumns(),
                    true
            );
        }
        copiedDB.setVersion(DatabaseConstants.DBVERSION);
        copiedDB.setTransactionSuccessful();
        copiedDB.endTransaction();
        //5. Cleanup
        copiedDB.close();
        preservedDB.close();
        preservedDBPath.delete();
    }

    private static void copyAssetFile(Context context) {
        int buffer_size = 8192;
        byte[] buffer = new byte[buffer_size];
        int bytes_read = 0;
        try {
            InputStream fis = context.getAssets().open(DBNAME);
            OutputStream os = new FileOutputStream(new File(context.getDatabasePath(DBNAME).getPath()));
            while ((bytes_read = fis.read(buffer)) > 0) {
                os.write(buffer,0,bytes_read);
            }
            os.flush();
            os.close();
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("Unable to copy from assets");
        }
    }

    private static int getDBVersion(Context context, String databaseName) {
        SQLiteDatabase db = SQLiteDatabase.openDatabase( context.getDatabasePath(databaseName).getPath(),null,SQLiteDatabase.OPEN_READONLY);
        int rv = db.getVersion();
        db.close();
        return rv;
    }
    private static void setDBVersion(Context context, String databaseName, int version) {
        SQLiteDatabase db = SQLiteDatabase.openDatabase( context.getDatabasePath(databaseName).getPath(),null,SQLiteDatabase.OPEN_READWRITE);
        db.setVersion(version);
        db.close();
    }

    private static boolean preserveTableColumns(
            SQLiteDatabase originalDatabase,
            SQLiteDatabase newDatabase,
            String tableName,
            String[] columnsToPreserve,
            String[] columnsToExtract,
            String[] whereClauseColumns,
            boolean failWithException) {

        StringBuilder sb = new StringBuilder();
        Cursor csr = originalDatabase.query("sqlite_master",new String[]{"name"},"name=? AND type=?",new String[]{tableName,"table"},null,null,null);
        if (!csr.moveToFirst()) {
            sb.append("\n\tTable ").append(tableName).append(" not found in database ").append(originalDatabase.getPath());
        }
        csr = newDatabase.query("sqlite_master",new String[]{"name"},"name=? AND type=?",new String[]{tableName,"table"},null,null,null);
        if (!csr.moveToFirst()) {
            sb.append("\n\tTable ").append(tableName).append(" not found in database ").append(originalDatabase.getPath());
        }
        if (sb.length() > 0) {
            if (failWithException) {
                throw new RuntimeException("Both databases are required to have a table named " + tableName + sb.toString());
            }
            return false;
        }
        for (String pc: columnsToPreserve) {
            boolean preserveColumnInExtractedColumn = false;
            for (String ec: columnsToExtract) {
                if (pc.equals(ec)) preserveColumnInExtractedColumn = true;
            }
            if (!preserveColumnInExtractedColumn) {
                if (failWithException) {
                    StringBuilder sbpc = new StringBuilder().append("Column in Columns to Preserve not found in Columns to Extract. Cannot continuue." +
                            "\n\tColumns to Preserve are :-");

                    }
                throw new RuntimeException("Column " + pc + " is not int the Columns to Extract.");
            }
            return false;
        }
        sb = new StringBuilder();
        for (String c: whereClauseColumns) {
            sb.append(c).append("=? ");
        }
        String[] whereargs = new String[whereClauseColumns.length];
        csr = originalDatabase.query(tableName,columnsToExtract,sb.toString(),whereClauseColumns,null,null,null);
        ContentValues cv = new ContentValues();
        while (csr.moveToNext()) {
            cv.clear();
            for (String pc: columnsToPreserve) {
                switch (csr.getType(csr.getColumnIndex(pc))) {
                    case Cursor.FIELD_TYPE_INTEGER:
                        cv.put(pc,csr.getLong(csr.getColumnIndex(pc)));
                        break;
                    case Cursor.FIELD_TYPE_STRING:
                        cv.put(pc,csr.getString(csr.getColumnIndex(pc)));
                        break;
                    case Cursor.FIELD_TYPE_FLOAT:
                        cv.put(pc,csr.getDouble(csr.getColumnIndex(pc)));
                        break;
                    case Cursor.FIELD_TYPE_BLOB:
                        cv.put(pc,csr.getBlob(csr.getColumnIndex(pc)));
                }
            }
            int waix = 0;
            for (String wa: whereClauseColumns) {
                whereargs[waix] = csr.getString(csr.getColumnIndex(wa));
            }
            newDatabase.update(tableName,cv,sb.toString(),whereargs);
        }
        csr.close();
        return true;
    }
}
0 голосов
/ 09 января 2020

Я немного отладил и изменил код с помощью MikeT, теперь вот последняя библиотека kotlin с простой databaseBuilder

https://github.com/ueen/RoomAssetHelper

Пожалуйста прочитайте документацию и сообщите, если у вас возникнут проблемы, наслаждайтесь:)

...