createFromAsset 迁移但保留特定列

createFromAsset Migration but keep specific Columns

我有一个测验应用程序,我有一个数据库,其中包含 table 中的所有问题,对于每个问题,如果答案正确,我会更新一列 solved,所以我可以用 SQL WHERE 过滤只显示未解决的问题。 现在每隔一段时间我就必须纠正问题中的错别字或者可能想添加一些新的错别字,所以

如何在保留 solved 的同时,将更正后的数据库 (questions.db) 从资产应用到用户设备上保存的数据库?

我想到并尝试了以下方法但没有成功:

所以从本质上讲,这可能是对 Room 开发团队的启发,我希望有一个适当的 createFromAsset 迁移策略,能够指定要保留的某些 columns/tables。 感谢您迄今为止所做的出色工作,我真的很喜欢使用 Android Jetpack 和 Room,尤其是! 另外,我很高兴可以采用任何解决方法来解决此问题:)

我相信以下是你想要的

@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,但该解决方案在使用 Cursor 更新新数据库时将原始数据库和新数据库分开。

一些 flexibility/adaptability 已包含在要保留的列中,可以对其进行扩展。在测试中 运行s 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时,从assets中复制数据库的第一个版本

    • 请注意,根据

    • ,此原创包含 3 个问题
    • 部分代码(在调用activity时检查是否所有已解决的值都为0,如果是,则更改id为2的问题的已解决状态)
  • 不复制数据库,而是在 DBVERSION 为 1 时使用现有数据库 运行(s)。
    • ID 2 仍然解决。

  • 将原始资产重命名为前缀original_后,编辑数据库如下,并将其复制到资产文件后:-

  • 不改变DBVERSION(还是1)运行并且原数据库还在使用

  • 将 DBVERSION 更改为 2 后 运行ning 复制更改的资产文件和 restores/preserves 已解决的状态。

  • 对于随后的 运行s,新数据的已解决状态保持不变。

为了测试调用 activity 包括:-

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();
        }
    }
} 

对于每个 运行,它会将所有问题输出到日志中两次。在第一个之后,如果没有解决的问题,它会解决 id 为 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

附加 - 改进版本[​​=56=]

这是一个经过批准的版本,可满足多个 table 和列的需求。为了满足 tables,添加了 class TablePreserve,允许 table、要保留的列、要提取的列和用于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
    };
}

然后 QuestionsDatabase 变成:-

@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;
    }
}

我对 MikeT 的代码进行了一些调试和修改,现在这是最终的 kotlin 库,带有一个简单的 databaseBuilder

https://github.com/ueen/RoomAssetHelper

如果您遇到任何问题,请阅读文档和报告,享受 :)