1. APP開發(fā)期間的數(shù)據(jù)庫
在上一篇文章中封裝了GreenDaoManager
中有個(gè)構(gòu)造方法如下:
private GreenDaoManager(){
if (mInstance == null) {
// DaoMaster.DevOpenHelper devOpenHelper = new
// DaoMaster.DevOpenHelper(MyApplication.getContext(), "database_name", null);//此處openhelper為自動生成開發(fā)所使用窟感,發(fā)布版本需自定義
MySQLiteOpenHelper devOpenHelper = new
MySQLiteOpenHelper(new GreenDaoContext(), "database_name.db", null);//GreenDaoContext為創(chuàng)建數(shù)據(jù)庫路徑使用
mDaoMaster = new DaoMaster(devOpenHelper.getWritableDatabase());
mDaoSession = mDaoMaster.newSession();
}
}
其中注釋掉的兩行代碼中:
DaoMaster.DevOpenHelper devOpenHelper = new
DaoMaster.DevOpenHelper(MyApplication.getContext(), "database_name", null);//此處DevOpenHelper 為自動生成開發(fā)所使用,發(fā)布版本需自定義
其中的MyApplication.getContext()
上下文表示了數(shù)據(jù)庫存儲路徑為手機(jī)內(nèi)存较锡。這里的DevOpenHelper
即為DaoMaster
中自動生成代碼打肝,完整代碼如下:
/** * WARNING: Drops all table on Upgrade! Use only during development. */
public static class DevOpenHelper extends OpenHelper {
public DevOpenHelper(Context context, String name, CursorFactory factory) {
super(context, name, factory);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i("greenDAO", "Upgrading schema from version " + oldVersion + " to " + newVersion + " by dropping all tables");
dropAllTables(db, true);
onCreate(db);
}
}
注意看第一行注釋:WARNING: Drops all table on Upgrade! Use only during development.
數(shù)據(jù)庫升級的話弄痹,會刪除所有表,然后重新創(chuàng)建园细。這種方式在開發(fā)期間耗式,APP還沒有上線之前是可以的。
當(dāng)APP上線后五芝,我們不能使用這種方式痘儡,因?yàn)檫@樣會導(dǎo)致已經(jīng)存在的數(shù)據(jù)會被刪除。
2. APP上線后,數(shù)據(jù)庫升級
我們需要重寫一個(gè)類MySQLiteOpenHelper
實(shí)現(xiàn)OpenHelper
.
- 需要自己實(shí)現(xiàn)了
onUpgrade
方法來自定義升級過程与柑。 - 當(dāng)然升級過程中也要修改
DaoMaster.SCHEMA_VERSION
- 當(dāng)
DaoMaster.SCHEMA_VERSION
跟你當(dāng)前數(shù)據(jù)庫的版本比較后,會根據(jù)你當(dāng)前數(shù)據(jù)庫的版本谤辜,然后進(jìn)行升級。 - 關(guān)鍵代碼
onUpgrade
方法,會比較新數(shù)據(jù)庫和舊數(shù)據(jù)庫的版本,然后執(zhí)行相應(yīng)的sql升級:
public class MySQLiteOpenHelper extends DaoMaster.OpenHelper {
public MySQLiteOpenHelper(Context context, String name) {
super(context, name);
}
public MySQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
super(context, name, factory);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
//數(shù)據(jù)遷移模塊
MigrationHelper.migrate(db,
UserDao.class,
ProfessionDao.class,
LTestDao.class);
}
}
數(shù)據(jù)庫升級不外乎兩種情況:
1.修改表結(jié)構(gòu)价捧。
2.添加新表。
針對onUpgrade
中的實(shí)現(xiàn)方法可以采用兩種不同策略:(稍后都會提供兩種策略的代碼實(shí)現(xiàn)涡戳,下面主要講述第二種數(shù)據(jù)遷移實(shí)現(xiàn)方式)
1.逐級版本迭代升級结蟋,比如當(dāng)前版本為1,最新版本為3渔彰,此方案就是先從1-->2-->3.
2.把數(shù)據(jù)庫中的數(shù)據(jù)備份嵌屎,然后全刪數(shù)據(jù)庫推正,再重新創(chuàng)建新的數(shù)據(jù)庫,把備份數(shù)據(jù)導(dǎo)入宝惰。此方案直接從1-->3植榕。
3. 數(shù)據(jù)遷移(數(shù)據(jù)庫備份)的具體實(shí)現(xiàn)
- android上面對的數(shù)據(jù)庫基本上就是輕量級數(shù)據(jù)庫
sqlite
,首先我們需要了解一些后面需要用到的數(shù)據(jù)庫操作語句:
- 判斷
sqlite
表是否存在
SELECT * FROM sqlite_master WHERE type='table' AND name='tempTableName';
-
sqlite
判斷臨時(shí)表是否存在
SELECT * FROM sqlite_temp_master WHERE type='table' AND name='tempTableName';
-
sqlite
創(chuàng)建表tableName
的臨時(shí)表
create temporary table tempTableName as select * from tableName;
- 刪除臨時(shí)表
drop table tempTableName;
-
sqlite的系統(tǒng)表
sqlite_master
、臨時(shí)表sqlite_temp_master
SQLite數(shù)據(jù)庫中一個(gè)特殊的名叫SQLITE_MASTER
上執(zhí)行一個(gè)SELECT查詢以獲得所有表的索引尼夺。每一個(gè) SQLite 數(shù)據(jù)庫都有一個(gè)叫SQLITE_MASTER
的表尊残, 它定義數(shù)據(jù)庫的模式。SQLITE_MASTER
表看起來如下:
CREATE TABLE sqlite_master (
type TEXT,
name TEXT,
tbl_name TEXT,
rootpage INTEGER,
sql TEXT
);
type 字段永遠(yuǎn)是 ‘table’淤堵,name 字段永遠(yuǎn)是對應(yīng)表的名字寝衫。所以,要獲得數(shù)據(jù)庫中所有表的列表拐邪, 使用下列SELECT語句:
SELECT name FROM sqlite_master
WHERE type=’table’
ORDER BY name;
對于索引慰毅,type 等于 ‘index’, name 則是索引的名字,tbl_name 是該索引所屬的表的名字扎阶。 不管是表還是索引汹胃,sql 字段是原先用 CREATE TABLE 或 CREATE INDEX 語句創(chuàng)建它們時(shí)的命令文本。對于自動創(chuàng)建的索引(用來實(shí)現(xiàn) PRIMARY KEY 或 UNIQUE 約束)东臀,sql字段為NULL着饥。
SQLITE_MASTER 表是只讀的。不能對它使用 UPDATE啡邑、INSERT 或 DELETE贱勃。 它會被 CREATE TABLE、CREATE INDEX谤逼、DROP TABLE 和 DROP INDEX 命令自動更新贵扰。
臨時(shí)表及其索引和觸發(fā)器存放在另外一個(gè)叫 SQLITE_TEMP_MASTER 的表中。SQLITE_TEMP_MASTER 跟 SQLITE_MASTER 差不多流部, 但它只是對于創(chuàng)建那些臨時(shí)表的應(yīng)用可見戚绕。如果要獲得所有表的列表, 不管是永久的還是臨時(shí)的枝冀,可以使用類似下面的命令:
SELECT name FROM
(SELECT * FROM sqlite_master UNION ALL
SELECT * FROM sqlite_temp_master)
WHERE type=’table’
ORDER BY name
-
數(shù)據(jù)遷移的工具類
MigrationHelper
舞丛。
public final class MigrationHelper {
public static boolean DEBUG = true;
private static String TAG = "MigrationHelper";
private static final String SQLITE_MASTER = "sqlite_master";
private static final String SQLITE_TEMP_MASTER = "sqlite_temp_master";
public static void migrate(SQLiteDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
Database database = new StandardDatabase(db);printLog("【The Old Database Version】" + db.getVersion()); printLog("【Generate temp table】start"); generateTempTables(database, daoClasses); printLog("【Generate temp table】complete"); dropAllTables(database, true, daoClasses); createAllTables(database, false, daoClasses); printLog("【Restore data】start"); restoreData(database, daoClasses); printLog("【Restore data】complete"); } private static void generateTempTables(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) { for (int i = 0; i < daoClasses.length; i++) { String tempTableName = null; DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]); String tableName = daoConfig.tablename; if (!isTableExists(db, false, tableName)) {//不存在系統(tǒng)表中,表明是新增表果漾,不需要?jiǎng)?chuàng)建臨時(shí)表 printLog("【New Table】" + tableName); continue; } try { tempTableName = daoConfig.tablename.concat("_TEMP"); StringBuilder dropTableStringBuilder = new StringBuilder(); dropTableStringBuilder.append("DROP TABLE IF EXISTS ").append(tempTableName).append(";"); db.execSQL(dropTableStringBuilder.toString()); StringBuilder insertTableStringBuilder = new StringBuilder(); insertTableStringBuilder.append("CREATE TEMPORARY TABLE ").append(tempTableName); insertTableStringBuilder.append(" AS SELECT * FROM ").append(tableName).append(";"); db.execSQL(insertTableStringBuilder.toString()); printLog("【Table】" + tableName +"\n ---Columns-->"+getColumnsStr(daoConfig)); printLog("【Generate temp table】" + tempTableName); } catch (SQLException e) { Log.e(TAG, "【Failed to generate temp table】" + tempTableName, e); } } } private static boolean isTableExists(Database db, boolean isTemp, String tableName) { if (db == null || TextUtils.isEmpty(tableName)) { return false; } String dbName = isTemp ? SQLITE_TEMP_MASTER : SQLITE_MASTER; String sql = "SELECT COUNT(*) FROM " + dbName + " WHERE type = ? AND name = ?"; Cursor cursor=null; int count = 0; try { cursor = db.rawQuery(sql, new String[]{"table", tableName}); if (cursor == null || !cursor.moveToFirst()) { return false; } count = cursor.getInt(0); } catch (Exception e) { e.printStackTrace(); } finally { if (cursor != null) cursor.close(); } return count > 0; } private static String getColumnsStr(DaoConfig daoConfig) { if (daoConfig == null) { return "no columns"; } StringBuilder builder = new StringBuilder(); for (int i = 0; i < daoConfig.allColumns.length; i++) { builder.append(daoConfig.allColumns[i]); builder.append(","); } if (builder.length() > 0) { builder.deleteCharAt(builder.length() - 1); } return builder.toString(); } private static void dropAllTables(Database db, boolean ifExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) { reflectMethod(db, "dropTable", ifExists, daoClasses); printLog("【Drop all table】"); } private static void createAllTables(Database db, boolean ifNotExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) { reflectMethod(db, "createTable", ifNotExists, daoClasses); printLog("【Create all table】"); } /** * dao class already define the sql exec method, so just invoke it */ private static void reflectMethod(Database db, String methodName, boolean isExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) { if (daoClasses.length < 1) { return; } try { for (Class cls : daoClasses) { Method method = cls.getDeclaredMethod(methodName, Database.class, boolean.class); method.invoke(null, db, isExists); } } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } private static void restoreData(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) { for (int i = 0; i < daoClasses.length; i++) { DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]); String tableName = daoConfig.tablename; String tempTableName = daoConfig.tablename.concat("_TEMP"); if (!isTableExists(db, true, tempTableName)) { continue; } try { // get all columns from tempTable, take careful to use the columns list List<String> columns = getColumns(db, tempTableName); ArrayList<String> properties = new ArrayList<>(columns.size()); for (int j = 0; j < daoConfig.properties.length; j++) { String columnName = daoConfig.properties[j].columnName; if (columns.contains(columnName)) { properties.add(columnName); } } if (properties.size() > 0) { final String columnSQL = TextUtils.join(",", properties); StringBuilder insertTableStringBuilder = new StringBuilder(); insertTableStringBuilder.append("INSERT INTO ").append(tableName).append(" ("); insertTableStringBuilder.append(columnSQL); insertTableStringBuilder.append(") SELECT "); insertTableStringBuilder.append(columnSQL); insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";"); db.execSQL(insertTableStringBuilder.toString()); printLog("【Restore data】 to " + tableName); } StringBuilder dropTableStringBuilder = new StringBuilder(); dropTableStringBuilder.append("DROP TABLE ").append(tempTableName); db.execSQL(dropTableStringBuilder.toString()); printLog("【Drop temp table】" + tempTableName); } catch (SQLException e) { Log.e(TAG, "【Failed to restore data from temp table 】" + tempTableName, e); } } } private static List<String> getColumns(Database db, String tableName) { List<String> columns = null; Cursor cursor = null; try { cursor = db.rawQuery("SELECT * FROM " + tableName + " limit 0", null); if (null != cursor && cursor.getColumnCount() > 0) { columns = Arrays.asList(cursor.getColumnNames()); } } catch (Exception e) { e.printStackTrace(); } finally { if (cursor != null) cursor.close(); if (null == columns) columns = new ArrayList<>(); } return columns; } private static void printLog(String info){ if(DEBUG){ Log.d(TAG, info); } } }
4. 設(shè)置數(shù)據(jù)庫路徑
在第一模塊中有句代碼:
MySQLiteOpenHelper devOpenHelper = new MySQLiteOpenHelper(new GreenDaoContext(), "database_name.db", null);//GreenDaoContext為創(chuàng)建數(shù)據(jù)庫路徑使用
里面有個(gè)上下文GreenDaoContext
繼承了ContextWrapper
,里面設(shè)置了數(shù)據(jù)庫路徑球切,代碼如下:
public class GreenDaoContext extends ContextWrapper {
private String currentUserId = "greendao";//一般用來針對一個(gè)用戶一個(gè)數(shù)據(jù)庫,以免數(shù)據(jù)混亂問題
private Context mContext;
public GreenDaoContext() {
super(MyApplication.getContext());
this.mContext = MyApplication.getContext();
// this.currentUserId = "greendao";//初始化
}
/**
* 獲得數(shù)據(jù)庫路徑绒障,如果不存在吨凑,則創(chuàng)建對象
*
* @param dbName
*/
@Override
public File getDatabasePath(String dbName) {
String dbDir = CommonUtils.getDBPath();
if (TextUtils.isEmpty(dbDir)){
Log.e("SD卡管理:", "SD卡不存在,請加載SD卡");
return null;
}
File baseFile = new File(dbDir);
// 目錄不存在則自動創(chuàng)建目錄
if (!baseFile.exists()){
baseFile.mkdirs();
}
StringBuffer buffer = new StringBuffer();
buffer.append(baseFile.getPath());
buffer.append(File.separator);
buffer.append(currentUserId);
dbDir = buffer.toString();// 數(shù)據(jù)庫所在目錄
buffer.append(File.separator);
// buffer.append(dbName+"_"+currentUserId);//也可以采用此種方式,將用戶id與表名聯(lián)系到一塊命名
buffer.append(dbName);
String dbPath = buffer.toString();// 數(shù)據(jù)庫路徑
// 判斷目錄是否存在鸵钝,不存在則創(chuàng)建該目錄
File dirFile = new File(dbDir);
if (!dirFile.exists()){
dirFile.mkdirs();
}
// 數(shù)據(jù)庫文件是否創(chuàng)建成功
boolean isFileCreateSuccess = false;
// 判斷文件是否存在糙臼,不存在則創(chuàng)建該文件
File dbFile = new File(dbPath);
if (!dbFile.exists()) {
try {
isFileCreateSuccess = dbFile.createNewFile();// 創(chuàng)建文件
} catch (IOException e) {
e.printStackTrace();
}
} else
isFileCreateSuccess = true;
// 返回?cái)?shù)據(jù)庫文件對象
if (isFileCreateSuccess)
return dbFile;
else
return super.getDatabasePath(dbName);
}
/**
* 重載這個(gè)方法,是用來打開SD卡上的數(shù)據(jù)庫的恩商,android 2.3及以下會調(diào)用這個(gè)方法变逃。
*
* @param name
* @param mode
* @param factory
*/
@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode,
SQLiteDatabase.CursorFactory factory) {
SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), factory);
return result;
}
/**
* Android 4.0會調(diào)用此方法獲取數(shù)據(jù)庫。
*
* @param name
* @param mode
* @param factory
* @param errorHandler
* @see android.content.ContextWrapper#openOrCreateDatabase(java.lang.String, int,
* android.database.sqlite.SQLiteDatabase.CursorFactory,
* android.database.DatabaseErrorHandler)
*/
@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory,
DatabaseErrorHandler errorHandler) {
SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), factory);
return result;
}
}
OK怠堪,先到這了揽乱。。研叫。
源碼奉上锤窑,自行參考