无法从资产中复制预先创建的数据库

Can't copy pre-created db from assets

android 无法从资产复制我预先创建的数据库,旧数据库仍然存在于 data/data/packagename/dabases,我想用新数据库更改旧数据库

ive tried to change this.getReadableDatabase(); into SQLiteDatabse db = this.getReadabledatabase()
if (db.isOpen()){
    db.close();
}


 public void createDatabase(){
   boolean dbExist = checkDatabase();
   if(!dbExist){
       this.getReadableDatabase();
       SQl
       try {
           copyDatabase();
       }catch (IOException e){
           Log.e(this.getClass().toString(),"eror kopi");
           throw new Error("error kopi");
       }
   }else{
       Log.i(this.getClass().toString(),"databse udah ada");
   }
}                                                                   
    private void copyDatabase() throws IOException {
    InputStream externalDBStream = 
     context.getAssets().open(DATABASE_NAME);
    String outFileName = DB_PATH + DATABASE_NAME;
    OutputStream localDBStream = new FileOutputStream(outFileName);
    byte[] buffer = new byte[1024];
    int bytesRead;
    while ((bytesRead = externalDBStream.read(buffer)) > 0) {
        localDBStream.write(buffer, 0, bytesRead);
    }
    localDBStream.flush();
    localDBStream.close();
    externalDBStream.close();
}

我希望使用我预先创建的数据库

你有很多问题,假设你想用另一个副本替换现有的预先存在的数据库。

您面临的问题是,由于存在数据库,因此复制将不会继续,即 checkDatabase() 将 return 为真。

如果您只是简单地调用 copyDatabase(),那么每次应用 运行 时都会复制数据库,如果数据库可以由用户修改。

你需要做的是有一个指标,可以测试,看看是否已经改变了预先存在的数据库。有多种方法,但最 likely/common 的方法是利用 SQLite user_version。这是一个整数值,经常用于通过 onUpgrade 方法更新当前数据库。

作为打开数据库的一部分,SQLiteOpenHelper(因此是其子类)将存储在数据库中的 user_version 与提供的版本号([= 的第 4 个参数)进行比较122=]iteOpenHelper 超级调用),如果后者大于数据库中存储的值,则调用 onUpgrade 方法。 (如果相反,那么 onDowngrade 方法将被调用,并且在没有被编码的​​情况下会发生异常)。

user_version可以在SQL站点管理工具用户SQLPRAGMA user_version = n.

中设置

另一个问题是,从Android9开始,数据库默认以WAL(Write-Ahead Logging)模式打开。使用 this.getReadableDatabase(); 的上述代码导致创建 -shm 和 -wal 文件。它们的存在会导致陷入错误(因为它们与复制的数据库不匹配),然后导致 SQLiteOpenHelper 创建一个空的(理论上可用的数据库)基本上擦除复制的数据库(我相信这就是发生的事情).

之所以使用this.getReadableDatabase();是因为它解决了当没有应用程序数据时,数据库 folder/directory没有的问题t 存在并使用上面的创建它。正确的方法是创建数据库 directory/folder 如果它不存在。因此,不会创建 -wal 和 -shm 文件。

以下是一个 DatabseHelper 示例,它克服了这些问题,并且还允许根据更改 user_version.

复制预先存在的数据库的修改版本
public class DBHelperV001 extends SQLiteOpenHelper {

    public static final String DBNAME = "test.db"; //<<<<<<<<<< obviously change accordingly

    //
    private static int db_user_version, asset_user_version, user_version_offset = 60, user_version_length = 4;
    private static String stck_trc_msg = " (see stack-trace above)";
    private static String sqlite_ext_journal = "-journal";
    private static String sqlite_ext_shm = "-shm";
    private static String sqlite_ext_wal = "-wal";
    private static int copy_buffer_size = 1024 * 8; //Copy data in 8k chucks, change if wanted.

    SQLiteDatabase mDB;

    /**
     *  Instantiate the DBHelper, copying the databse from the asset folder if no DB exists
     *  or if the user_version is greater than the user_version of the current database.
     *  NOTE The pre-existing database copied into the assets folder MUST have the user version set
     *  to 1 or greater. If the user_version in the assets folder is increased above the
     *
     * @param context
     */
    public DBHelperV001(Context context) {

        // Note get the version according to the asset file
        // avoid having to maintain the version number passed
        super(context, DBNAME, null, setUserVersionFromAsset(context,DBNAME));
        if (!ifDbExists(context,DBNAME)) {
            copyDBFromAssets(context, DBNAME,DBNAME);
        } else {
            setUserVersionFromAsset(context,DBNAME);
            setUserVersionFromDB(context,DBNAME);
            if (asset_user_version > db_user_version) {
                copyDBFromAssets(context,DBNAME,DBNAME);
            }
        }
        // Force open (and hence copy attempt) when constructing helper
        mDB = this.getWritableDatabase();
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    }

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

    @Override
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }

    /**
     * Check to see if the databse file exists
     * @param context   The Context
     * @param dbname    The databse name
     * @return          true id database file exists, else false
     */
    private static boolean ifDbExists(Context context, String dbname) {
        File db = context.getDatabasePath(dbname);
        if (db.exists()) return true;
        if (!db.getParentFile().exists()) {
            db.getParentFile().mkdirs();
        }
        return false;
    }

    /**
     * set the db_user_version according to the user_version obtained from the current database file
     * @param context   The Context
     * @param dbname    The database (file) name
     * @return          The user_version
     */
    private static int setUserVersionFromDB(Context context, String dbname) {
        File db = context.getDatabasePath(dbname);
        InputStream is;
        try {
            is = new FileInputStream(db);
        } catch (IOException e) {
            throw new RuntimeException("IOError Opening " + db.getPath() + " as an InputStream" + stck_trc_msg);
        }
        db_user_version = getUserVersion(is);
        Log.d("DATABASEUSERVERSION","Obtained user_version from current DB, it is " + String.valueOf(db_user_version)); //TODO remove for live App
        return db_user_version;
    }

    /**
     * set the asset_user_version according to the user_version from the asset file
     * @param context
     * @param assetname
     * @return
     */
    private static int setUserVersionFromAsset(Context context, String assetname) {
        InputStream is;
        try {
            is = context.getAssets().open(assetname);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("IOError Getting asset " + assetname + " as an InputStream" + stck_trc_msg);
        }
        asset_user_version = getUserVersion(is);
        Log.d("ASSETUSERVERSION","Obtained user_version from asset, it is " + String.valueOf(asset_user_version)); //TODO remove for Live App
        return asset_user_version;
    }

    /**
     * Retrieve SQLite user_version from the provied InputStream
     * @param is    The InputStream
     * @return      the user_version
     */
    private static int getUserVersion(InputStream is) {
        String ioerrmsg = "Reading DB header bytes(60-63) ";
        int rv;
        byte[] buffer = new byte[user_version_length];
        byte[] header = new byte[64];
        try {
            is.skip(user_version_offset);
            is.read(buffer,0,user_version_length);
            ByteBuffer bb = ByteBuffer.wrap(buffer);
            rv = ByteBuffer.wrap(buffer).getInt();
            ioerrmsg = "Closing DB ";
            is.close();
            return rv;
        } catch (IOException e) {
            e.printStackTrace();
            throw  new RuntimeException("IOError " + ioerrmsg + stck_trc_msg);
        }
    }

    /**
     * Copy the database file from the assets 
     * Note backup of existing files may not be required
     * @param context   The Context
     * @param dbname    The database (file)name
     * @param assetname The asset name (may therefore be different but )
     */
    private static void copyDBFromAssets(Context context, String dbname, String assetname) {
        String tag = "COPYDBFROMASSETS";
        Log.d(tag,"Copying Database from assets folder");
        String backup_base = "bkp_" + String.valueOf(System.currentTimeMillis());
        String ioerrmsg = "Opening Asset " + assetname;

        // Prepare Files that could be used
        File db = context.getDatabasePath(dbname);
        File dbjrn = new File(db.getPath() + sqlite_ext_journal);
        File dbwal = new File(db.getPath() + sqlite_ext_wal);
        File dbshm = new File(db.getPath() + sqlite_ext_shm);
        File dbbkp = new File(db.getPath() + backup_base);
        File dbjrnbkp = new File(db.getPath() + backup_base);
        File dbwalbkp = new File(db.getPath() + backup_base);
        File dbshmbkp = new File(db.getPath() + backup_base);
        byte[] buffer = new byte[copy_buffer_size];
        int bytes_read = 0;
        int total_bytes_read = 0;
        int total_bytes_written = 0;

        // Backup existing sqlite files
        if (db.exists()) {
            db.renameTo(dbbkp);
            dbjrn.renameTo(dbjrnbkp);
            dbwal.renameTo(dbwalbkp);
            dbshm.renameTo(dbshmbkp);
        }
        // ALWAYS delete the additional sqlite log files
        dbjrn.delete();
        dbwal.delete();
        dbshm.delete();

        //Attempt the copy
        try {
            ioerrmsg = "Open InputStream for Asset " + assetname;
            InputStream is = context.getAssets().open(assetname);
            ioerrmsg = "Open OutputStream for Databse " + db.getPath();
            OutputStream os = new FileOutputStream(db);
            ioerrmsg = "Read/Write Data";
             while((bytes_read = is.read(buffer)) > 0) {
                 total_bytes_read = total_bytes_read + bytes_read;
                 os.write(buffer,0,bytes_read);
                 total_bytes_written = total_bytes_written + bytes_read;
             }
             ioerrmsg = "Flush Written data";
             os.flush();
             ioerrmsg = "Close DB OutputStream";
             os.close();
             ioerrmsg = "Close Asset InputStream";
             is.close();
             Log.d(tag,"Databsse copied from the assets folder. " + String.valueOf(total_bytes_written) + " bytes were copied.");
             // Delete the backups
             dbbkp.delete();
             dbjrnbkp.delete();
             dbwalbkp.delete();
             dbshmbkp.delete();
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("IOError attempting to " + ioerrmsg + stck_trc_msg);
        }
    }
}

用法示例

考虑以下资产文件(sqlite 数据库)(警告,因为它们是应用程序将失败):-

所以有两个数据库(使用 PRAGMA user_version = 1PRAGMA user_version = 2 respectively/according 设置的 user_version 与文件名相同) 对于全新的,第一次 运行 应用程序(即卸载)然后文件 test.dbV1 重命名为 test.db 并使用以下 activity :-

public class MainActivity extends AppCompatActivity {

    DBHelperV001 mDbhlpr;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDbhlpr = new DBHelperV001(this);
        DatabaseUtils.dumpCursor(
                mDbhlpr.getWritableDatabase().query(
                        "sqlite_master",
                        null,null,null,null,null,null
                )
        );
    }
}
  • 这只是实例化数据库助手(它将复制或使用数据库)然后转储 sqlite_master table.

日志包含:-

04-02 12:55:36.258 644-644/aaa.so55441840 D/ASSETUSERVERSION: Obtained user_version from asset, it is 1
04-02 12:55:36.258 644-644/aaa.so55441840 D/COPYDBFROMASSETS: Copying Database from assets folder
04-02 12:55:36.262 644-644/aaa.so55441840 D/COPYDBFROMASSETS: Databsse copied from the assets folder. 69632 bytes were copied.
04-02 12:55:36.265 644-644/aaa.so55441840 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@d121f9c
04-02 12:55:36.265 644-644/aaa.so55441840 I/System.out: 0 {
04-02 12:55:36.265 644-644/aaa.so55441840 I/System.out:    type=table
04-02 12:55:36.265 644-644/aaa.so55441840 I/System.out:    name=android_metadata
04-02 12:55:36.265 644-644/aaa.so55441840 I/System.out:    tbl_name=android_metadata
04-02 12:55:36.265 644-644/aaa.so55441840 I/System.out:    rootpage=3
04-02 12:55:36.265 644-644/aaa.so55441840 I/System.out:    sql=CREATE TABLE android_metadata (locale TEXT)
04-02 12:55:36.266 644-644/aaa.so55441840 I/System.out: }
04-02 12:55:36.266 644-644/aaa.so55441840 I/System.out: 1 {
04-02 12:55:36.266 644-644/aaa.so55441840 I/System.out:    type=table
04-02 12:55:36.266 644-644/aaa.so55441840 I/System.out:    name=shops
..........

引入新版本DB时,user_version为2

  • test.dbtest.dbV1 重命名为 test.dbV1 然后,
    • (有效删除)
  • test.dbV2然后重命名为test.db
    • (有效引入新资产文件)然后:-

然后应用程序重新运行然后日志包含:-

04-02 13:04:25.044 758-758/? D/ASSETUSERVERSION: Obtained user_version from asset, it is 2
04-02 13:04:25.046 758-758/? D/ASSETUSERVERSION: Obtained user_version from asset, it is 2
04-02 13:04:25.046 758-758/? D/DATABASEUSERVERSION: Obtained user_version from current DB, it is 1
04-02 13:04:25.047 758-758/? D/COPYDBFROMASSETS: Copying Database from assets folder
04-02 13:04:25.048 758-758/? D/COPYDBFROMASSETS: Databsse copied from the assets folder. 69632 bytes were copied.
04-02 13:04:25.051 758-758/? I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@25012da5
04-02 13:04:25.052 758-758/? I/System.out: 0 {
04-02 13:04:25.052 758-758/? I/System.out:    type=table
04-02 13:04:25.052 758-758/? I/System.out:    name=android_metadata
04-02 13:04:25.052 758-758/? I/System.out:    tbl_name=android_metadata
04-02 13:04:25.052 758-758/? I/System.out:    rootpage=3
04-02 13:04:25.052 758-758/? I/System.out:    sql=CREATE TABLE android_metadata (locale TEXT)
04-02 13:04:25.052 758-758/? I/System.out: }
04-02 13:04:25.052 758-758/? I/System.out: 1 {
04-02 13:04:25.052 758-758/? I/System.out:    type=table
04-02 13:04:25.052 758-758/? I/System.out:    name=shops

最后,随后的 运行 即没有更新的资产,日志显示:-

04-02 13:05:50.197 840-840/aaa.so55441840 D/ASSETUSERVERSION: Obtained user_version from asset, it is 2
04-02 13:05:50.198 840-840/aaa.so55441840 D/ASSETUSERVERSION: Obtained user_version from asset, it is 2
04-02 13:05:50.198 840-840/aaa.so55441840 D/DATABASEUSERVERSION: Obtained user_version from current DB, it is 2
04-02 13:05:50.201 840-840/aaa.so55441840 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@d121f9c
04-02 13:05:50.202 840-840/aaa.so55441840 I/System.out: 0 {
04-02 13:05:50.202 840-840/aaa.so55441840 I/System.out:    type=table
04-02 13:05:50.202 840-840/aaa.so55441840 I/System.out:    name=android_metadata
04-02 13:05:50.202 840-840/aaa.so55441840 I/System.out:    tbl_name=android_metadata
04-02 13:05:50.202 840-840/aaa.so55441840 I/System.out:    rootpage=3
04-02 13:05:50.202 840-840/aaa.so55441840 I/System.out:    sql=CREATE TABLE android_metadata (locale TEXT)
04-02 13:05:50.202 840-840/aaa.so55441840 I/System.out: }
04-02 13:05:50.202 840-840/aaa.so55441840 I/System.out: 1 {
04-02 13:05:50.202 840-840/aaa.so55441840 I/System.out:    type=table
04-02 13:05:50.202 840-840/aaa.so55441840 I/System.out:    name=shops

即没有复制,因为资产实际上是相同的