Masz wiele problemów, zakładając, że chcesz zastąpić istniejącą wcześniej bazę danych inną kopią.
Problem, z którym się zmagasz, polega na tym, że skoro baza danych istnieje, kopia nie będzie kontynuowana, tj. checkDatabase() zwróci prawdę.
Gdybyś miał po prostu wywołać copyDatabase() wtedy baza danych byłaby kopiowana przy każdym uruchomieniu aplikacji, co byłoby nieefektywne i destrukcyjne, gdyby baza danych mogła zostać zmodyfikowana przez użytkownika.
Musisz mieć wskaźnik, który można przetestować, aby sprawdzić, czy istniejąca wcześniej baza danych została zmieniona. Istnieją różne sposoby, ale najbardziej prawdopodobnym/powszechnym sposobem byłoby wykorzystanie wersji_użytkownika SQLite . Jest to wartość całkowita i jest często używana do aktualizacji bieżącej bazy danych za pomocą onUpgrade metoda.
W ramach otwierania bazy danych, SQLiteOpenHelper (a więc jej podklasa) porównuje wersję user_version przechowywaną w bazie danych z podanym numerem wersji (4 parametr do super wywołania SQLiteOpenHelper) i jeśli ten ostatni jest większy niż wartość przechowywana w bazy danych, wtedy wywoływana jest metoda onUpgrade. (jeśli jest odwrotnie, to onDowngrade zostanie wywołana metoda i bez jej zakodowania wystąpi wyjątek).
User_version można ustawić w narzędziu zarządzania SQLite użytkownik SQL PRAGMA user_version = n
.
Inną kwestią jest to, że od Androida 9 baza danych jest domyślnie otwierana w trybie WAL (Write-Ahead Logging). Powyższy kod za pomocą this.getReadableDatabase();
powoduje utworzenie plików -shm i -wal. Ich istnienie skutkuje uwięzionym błędem (ponieważ wtedy nie pasują do skopiowanej bazy danych), który następnie powoduje, że SQLiteOpenHelper tworzy pustą (teoretycznie użyteczną bazę danych) zasadniczo usuwając skopiowaną bazę danych (Wierzę, że tak się dzieje ).
Powód, dla którego this.getReadableDatabase();
jest używany, ponieważ pozwala ominąć problem polegający na tym, że gdy nie ma danych aplikacji, bazy danych folder/katalog nie istnieje, a użycie powyższego spowoduje jego utworzenie. Prawidłowym sposobem jest utworzenie katalogu/folderu bazy danych, jeśli nie istnieje. W związku z tym pliki -wal i -shm nie są tworzone.
Poniżej znajduje się przykład DatabseHelper, który rozwiązuje problemy i dodatkowo umożliwia kopiowanie zmodyfikowanych wersji istniejącej bazy danych w oparciu o zmianę 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);
}
}
}
Przykładowe użycie
Rozważ następujące pliki zasobów (bazy danych sqlite) (ostrzeżenie, ponieważ aplikacja ulegnie awarii ) :-
Tak więc istnieją dwie bazy danych (identyczny pasek wersja_użytkownika ustawiona za pomocą PRAGMA user_version = 1
i PRAGMA user_version = 2
odpowiednio/zgodnie z nazwami plików) W przypadku zupełnie nowej, po raz pierwszy uruchom aplikację (tj. odinstalowaną), a następnie plik test.dbV1 zmieniono nazwę na test.db i stosuje się następujące działanie:-
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
)
);
}
}
- To po prostu tworzy instancję Pomocnika bazy danych (który skopiuje lub użyje bazy danych), a następnie zrzuca tabelę sqlite_master.
Dziennik zawiera :-
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
..........
Po wprowadzeniu nowej wersji bazy danych, która ma wersję user_version 2
- tj. test.db który był test.dbV1 zmieniono nazwę na test.dbV1 I wtedy
- (efektywne usunięcie go)
- test.dbV2 zostanie następnie przemianowany na test.db
- (efektywnie wprowadzając nowy plik zasobów), a następnie :-
Aplikacja jest następnie uruchamiana ponownie, a dziennik zawiera:-
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
Wreszcie, przy kolejnym uruchomieniu, tj. bez zaktualizowanego zasobu, dziennik pokazuje:-
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
tj. żadna kopia nie została wykonana, ponieważ zasób jest faktycznie taki sam