Bạn gặp một số vấn đề, giả sử rằng bạn muốn thay thế cơ sở dữ liệu hiện có từ trước bằng một bản sao khác.
Vấn đề bạn đang gặp phải là khi cơ sở dữ liệu tồn tại thì bản sao sẽ không tiếp tục, tức là checkDatabase () sẽ trả về true.
Nếu bạn chỉ gọi copyDatabase () thì cơ sở dữ liệu sẽ được sao chép mỗi khi Ứng dụng được chạy, điều này sẽ không hiệu quả và có thể phá hủy nếu người dùng có thể sửa đổi cơ sở dữ liệu.
Những gì bạn cần làm là có một chỉ báo, có thể được kiểm tra, để xem liệu cơ sở dữ liệu tồn tại trước đó có bị thay đổi hay không. Có nhiều cách khác nhau nhưng cách phổ biến nhất là sử dụng user_version của SQLite . Đây là một giá trị số nguyên và thường được sử dụng để cập nhật cơ sở dữ liệu hiện tại qua onUpgrade phương pháp.
Là một phần của việc mở cơ sở dữ liệu, SQLiteOpenHelper (và do đó là một lớp con của nó) nó so sánh user_version được lưu trữ trong cơ sở dữ liệu với số phiên bản được cung cấp (tham số thứ 4 của lệnh gọi siêu SQLiteOpenHelper) và nếu cái sau lớn hơn giá trị được lưu trữ trong cơ sở dữ liệu thì phương thức onUpgrade được gọi. (nếu ngược lại thì onDowngrade phương thức sẽ được gọi và nếu không có mã thì một ngoại lệ sẽ xảy ra).
User_version có thể được đặt trong người dùng công cụ quản lý SQLite là SQL PRAGMA user_version = n
.
Một vấn đề khác là từ Android 9, cơ sở dữ liệu được mở ở chế độ WAL (Ghi nhật ký phía trước) theo mặc định. Đoạn mã trên bằng cách sử dụng this.getReadableDatabase();
kết quả là các tệp -shm và -wal được tạo. Sự tồn tại của chúng dẫn đến một lỗi bị mắc kẹt (vì sau đó chúng không khớp với cơ sở dữ liệu đã sao chép) dẫn đến việc SQLiteOpenHelper tạo ra một cơ sở dữ liệu trống (về mặt lý thuyết có thể sử dụng) xóa sạch cơ sở dữ liệu đã sao chép ( Tôi tin rằng đây là điều sẽ xảy ra ).
Lý do tại sao this.getReadableDatabase();
đã được sử dụng là nó giải quyết được vấn đề khi không có dữ liệu Ứng dụng, cơ sở dữ liệu thư mục / thư mục không tồn tại và việc sử dụng ở trên sẽ tạo ra nó. Cách đúng là tạo thư mục / thư mục cơ sở dữ liệu nếu nó không tồn tại. Do đó, các tệp -wal và -shm không được tạo.
Sau đây là một ví dụ DatabseHelper khắc phục được các vấn đề và cũng cho phép sao chép các phiên bản đã sửa đổi của cơ sở dữ liệu có trước dựa trên việc thay đổi 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);
}
}
}
Cách sử dụng mẫu
Hãy xem xét các tệp nội dung sau (cơ sở dữ liệu sqlite) ( cảnh báo vì chúng là ứng dụng sẽ không thành công ):-
Vì vậy, có hai cơ sở dữ liệu (thanh giống hệt user_version như được đặt bằng cách sử dụng PRAGMA user_version = 1
và PRAGMA user_version = 2
tương ứng / theo tên tệp) Đối với thương hiệu mới, lần đầu tiên hãy chạy Ứng dụng (tức là đã gỡ cài đặt) rồi đến tệp test.dbV1 được đổi tên thành test.db và hoạt động sau được sử dụng:-
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
)
);
}
}
- Điều này chỉ đơn giản là khởi chạy Trình trợ giúp cơ sở dữ liệu (sẽ sao chép hoặc sử dụng cơ sở dữ liệu), sau đó kết xuất bảng sqlite_master.
Nhật ký chứa:-
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
..........
Khi phiên bản mới của DB được giới thiệu, phiên bản này có user_version là 2
- tức là test.db đó là test.dbV1 được đổi tên thành test.dbV1 VÀ sau đó,
- (xóa nó một cách hiệu quả)
- test.dbV2 sau đó được đổi tên thành test.db
- (giới thiệu một cách hiệu quả tệp nội dung mới) sau đó:-
Và Ứng dụng sau đó được chạy lại sau đó nhật ký chứa:-
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
Cuối cùng, với lần chạy tiếp theo, tức là không có nội dung cập nhật, nhật ký hiển thị:-
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
tức là không có bản sao nào được thực hiện vì nội dung thực sự giống nhau