SQLite
 sql >> Datenbank >  >> RDS >> SQLite

Kann keine vorab erstellte Datenbank aus Assets kopieren

Sie haben eine Reihe von Problemen, vorausgesetzt, Sie möchten eine vorhandene, bereits vorhandene Datenbank durch eine andere Kopie ersetzen.

Das Problem, mit dem Sie konfrontiert sind, besteht darin, dass die Kopie nicht fortgesetzt wird, wenn eine Datenbank vorhanden ist, dh die checkDatabase() wird true zurückgeben.

Wenn Sie einfach copyDatabase() aufrufen würden dann würde die Datenbank jedes Mal kopiert, wenn die App ausgeführt wird, was ineffizient und destruktiv wäre, wenn die Datenbank vom Benutzer geändert werden könnte.

Was Sie tun müssen, ist ein Indikator, der getestet werden kann, um zu sehen, ob die bereits vorhandene Datenbank geändert wurde. Es gibt verschiedene Möglichkeiten, aber die wahrscheinlichste/gebräuchlichste wäre die Verwendung der SQLite user_version . Dies ist ein ganzzahliger Wert und wird häufig verwendet, um die aktuelle Datenbank über das onUpgrade zu aktualisieren Methode.

Als Teil des Öffnens der Datenbank vergleicht der SQLiteOpenHelper (und damit eine Unterklasse davon) die in der Datenbank gespeicherte user_version mit der gelieferten Versionsnummer (4. Parameter zum SQLiteOpenHelper-Superaufruf) und ob letztere größer ist als der in der Datenbank, dann wird die Methode onUpgrade aufgerufen. (wenn umgekehrt dann das onDowngrade -Methode aufgerufen wird und ohne dass sie codiert wird, tritt eine Ausnahme auf).

Die Benutzerversion kann im SQLite-Verwaltungstool auf den SQL-Benutzer PRAGMA user_version = n gesetzt werden .

Ein weiteres Problem ist, dass die Datenbank ab Android 9 standardmäßig im WAL-Modus (Write-Ahead Logging) geöffnet wird. Der obige Code mit this.getReadableDatabase(); führt dazu, dass die Dateien -shm und -wal erstellt werden. Ihre Existenz führt zu einem abgefangenen Fehler (da sie dann nicht mit der kopierten Datenbank übereinstimmen), der dann dazu führt, dass der SQLiteOpenHelper eine leere (theoretisch nutzbare Datenbank) erstellt und die kopierte Datenbank im Grunde löscht (Ich glaube, das passiert ).

Der Grund, warum this.getReadableDatabase(); verwendet wurde, um das Problem zu umgehen, dass wenn keine App-Daten vorhanden sind, die Datenbanken Ordner/Verzeichnis existiert nicht und die Verwendung der oben genannten erstellt es. Der richtige Weg ist, das Datenbankverzeichnis/den Datenbankordner zu erstellen, wenn es nicht existiert. Daher werden die Dateien -wal und -shm nicht erstellt.

Das Folgende ist ein Beispiel für einen DatabseHelper, der die Probleme überwindet und zusätzlich ermöglicht, modifizierte Versionen der bereits vorhandenen Datenbank zu kopieren, basierend auf einer Änderung der 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);
        }
    }
}

Beispielnutzung

Betrachten Sie die folgenden Assets-Dateien (SQLite-Datenbanken) (Warnung, da die App fehlschlagen würde ) :-

Es gibt also zwei Datenbanken (identisch bis auf die user_version wie sie mit PRAGMA user_version = 1 gesetzt wurde und PRAGMA user_version = 2 bzw. nach Dateinamen) Bei der brandneuen App zuerst starten (d.h. deinstalliert) dann Datei test.dbV1 wird in test.db umbenannt und die folgende Aktivität wird verwendet:-

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
                )
        );
    }
}
  • Dies instanziiert einfach den Database Helper (der die Datenbank kopiert oder verwendet) und gibt dann die Tabelle sqlite_master aus.

Das Protokoll enthält:-

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 [email protected]
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
..........

Wenn die neue Version der DB eingeführt wird, die eine user_version von 2 hat

  • d.h. test.db das war test.dbV1 wird in test.dbV1 umbenannt UND dann,
    • (effektiv löschen)
  • test.dbV2 wird dann in test.db umbenannt
    • (effektive Einführung der neuen Asset-Datei) dann:-

Und die App wird dann erneut ausgeführt, dann enthält das Protokoll :-

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 [email protected]
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

Schließlich zeigt das Protokoll bei einem nachfolgenden Lauf, d. h. ohne aktualisiertes Asset, :-

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 [email protected]
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

d. h. es wird keine Kopie erstellt, da das Asset praktisch dasselbe ist