因為Greendao的高性能以及ORM框架的特點許多項目都是用了Greendao做為數(shù)據(jù)庫組件毡证,不僅提升了開發(fā)效率并且使很多程序員擺脫了枯燥的SQL語句(包括我),這個框架也是非常受歡迎的须鼎,但是我在使用過程中也發(fā)現(xiàn)了這個框架的一些不足
- 不支持線程回調
- 不支持默認數(shù)據(jù)類型
這兩點基本上是我使用這個ORM數(shù)據(jù)庫框架碰到的最失望的問題了,但是因為項目之前使用的就是該框架且不已我的意志為轉移的情況下只能繼續(xù)使用府蔗,并且在我這一段時間的使用中對這個框架也有一些經(jīng)驗和想法分享出來希望能幫到大家晋控,我來說一下針對這兩個問題有些什么方法
線程回調問題
這個問題不是這篇文章的重點,實際上我也沒有實際解決這個問題姓赤,但是有一些見解赡译,如果是我們自己手動為項目編寫數(shù)據(jù)庫組件的花一般都會遵循經(jīng)驗講數(shù)據(jù)庫操作放在子線程中進行并添加回調,但是遺憾的是Greendao項目組可能出于對該組件效率的自信并沒有這么做(事實也證明Greendao確實是所有ORM框架中效率最高的)模捂,但是 我們還是有這個需求的捶朵,比如筆者的項目中就有需求是直接從服務器獲取上萬條數(shù)據(jù)然后插入到本地或者直接從本地獲取上萬條數(shù)據(jù)(此處只想吐槽產(chǎn)品經(jīng)理的腦回路),這個時候如果在UI線程插入或讀取雖然相對來講其實也還算快狂男,但用戶總歸是會感覺到卡頓(大概一到兩秒综看,視實際機器性能不定),這總歸是很不爽的岖食,且不被接受红碑,存在ANR的風險
但是如果直接使用Greendao提供的異步方法我們又不知道何時插入完成,何時更新UI(這就很尷尬了),我們團隊的解決方案是犧牲部分用戶體驗后臺控制數(shù)據(jù)分批傳送,當然析珊,這其實是不得以的鴕鳥做法羡鸥,正確的做法是:研究一下Greendao的源碼并添加回調接口,當然這可能需要的不止一點時間忠寻,而且一般項目很少存在我這種一次性操作上萬條數(shù)據(jù)的情況惧浴,如果有,且在你們的項目還沒有上馬Greendao的情況下奕剃,趕緊棄暗投明 如果已經(jīng)上馬 趁項目崩潰前跑路吧
看到這里不要崩潰衷旅,開玩笑的,事情當然沒有那么嚴重纵朋,問題是可以解決的柿顶,我們大可以在外部包裹線程實現(xiàn)GreenDao的異步調用,無論是AsyncTask還是自己實現(xiàn)一個線程異步類操软,查詢時開啟一個線程嘁锯,查詢結果出來后再回調到主線程即可,工作量也不大聂薪,不過還是覺得GreenDao能提供的話還是要方便很多
當然也可以向大家介紹一款國產(chǎn)ORM數(shù)據(jù)庫框架LitePay家乘,由國內Android開發(fā)大神郭霖開源,在最近最新的一版更新中該框架已支持異步回調胆建,當然烤低,對于Greendao的異步本文不再多講肘交,畢竟本文最關心的另一個問題
數(shù)據(jù)庫升級問題
在項目中我們總會由各種各樣的問題需要對原有數(shù)據(jù)進行升級例如增加新的字段笆载,但是有的時候我們可能需又可能需要保留原來的數(shù)據(jù),在這里我不得不羨慕我的項目中負責另外的模塊的伙伴涯呻,他們的數(shù)據(jù)不僅能從服務器獲取且數(shù)據(jù)量極小凉驻,每次刪除后都可以從服務器重新獲取,根本不用擔心保留數(shù)據(jù)的問題复罐,所以他們一般采用直接刪除舊表再創(chuàng)建新表的方法應對數(shù)據(jù)庫版本升級涝登,有點小羨慕
但是 這種方式畢竟過于簡單粗暴且不夠優(yōu)雅,最重要的是并不適合我的模塊效诅,我一開始的思路是先取出數(shù)據(jù)保留再內存中胀滚,然后將舊數(shù)據(jù)通過添加新字段默認值的方式升級為新表適用的數(shù)據(jù),然后刪除舊表乱投,創(chuàng)建新標咽笼,將轉換后的數(shù)據(jù)插入到新表中,但是不知道為什么這種方式看似沒毛财蒽拧(肯定有毛步P獭)但是每次都會報錯(報錯信息沒保留下來),不得已只能尋找另外別的方法,找來找去施掏,發(fā)現(xiàn)有一種方法是通過升級時創(chuàng)建一個臨時表將數(shù)據(jù)保留下來钮惠,然后進行表的升級再,再將數(shù)據(jù)轉移到新表中來實現(xiàn)的七芭,這種方式和我的做法其實有點象素挽,但可能考慮得比我多,不是將數(shù)據(jù)留在內存中狸驳,而且經(jīng)過實測實際有用毁菱,我將代碼貼出來大家可以方便取用
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;
import com.oppo.community.util.LogUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.internal.DaoConfig;
public class MigrationHelper {
private static final String CONVERSION_CLASS_NOT_FOUND_EXCEPTION = "MIGRATION HELPER - CLASS DOESN'T MATCH WITH THE CURRENT PARAMETERS";
private static MigrationHelper instance;
public static MigrationHelper getInstance() {
if (instance == null) {
instance = new MigrationHelper();
}
return instance;
}
private static List<String> getColumns(SQLiteDatabase db, String tableName) {
List<String> columns = new ArrayList<>();
Cursor cursor = null;
try {
cursor = db.rawQuery("SELECT * FROM " + tableName + " limit 1", null);
if (cursor != null) {
columns = new ArrayList<>(Arrays.asList(cursor.getColumnNames()));
}
} catch (Exception e) {
LogUtil.d(tableName, e.getMessage());
e.printStackTrace();
} finally {
if (cursor != null)
cursor.close();
}
return columns;
}
public void migrate(SQLiteDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
generateTempTables(db, daoClasses);
DaoMaster.dropAllTables(db, true);
DaoMaster.createAllTables(db, false);
restoreData(db, daoClasses);
}
private void generateTempTables(SQLiteDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
for (int i = 0; i < daoClasses.length; i++) {
DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]);
String divider = "";
String tableName = daoConfig.tablename;
String tempTableName = daoConfig.tablename.concat("_TEMP");
ArrayList<String> properties = new ArrayList<>();
StringBuilder createTableStringBuilder = new StringBuilder();
createTableStringBuilder.append("CREATE TABLE ").append(tempTableName).append(" (");
for (int j = 0; j < daoConfig.properties.length; j++) {
String columnName = daoConfig.properties[j].columnName;
if (getColumns(db, tableName).contains(columnName)) {
properties.add(columnName);
String type = null;
try {
type = getTypeByClass(daoConfig.properties[j].type);
} catch (Exception exception) {
exception.printStackTrace();
}
createTableStringBuilder.append(divider).append(columnName).append(" ").append(type);
if (daoConfig.properties[j].primaryKey) {
createTableStringBuilder.append(" PRIMARY KEY");
}
divider = ",";
}
}
createTableStringBuilder.append(");");
LogUtil.d("TAG", "創(chuàng)建臨時表的SQL語句: " + createTableStringBuilder.toString());
db.execSQL(createTableStringBuilder.toString());
StringBuilder insertTableStringBuilder = new StringBuilder();
insertTableStringBuilder.append("INSERT INTO ").append(tempTableName).append(" (");
insertTableStringBuilder.append(TextUtils.join(",", properties));
insertTableStringBuilder.append(") SELECT ");
insertTableStringBuilder.append(TextUtils.join(",", properties));
insertTableStringBuilder.append(" FROM ").append(tableName).append(";");
LogUtil.d("TAG", "在臨時表插入數(shù)據(jù)的SQL語句:" + insertTableStringBuilder.toString());
db.execSQL(insertTableStringBuilder.toString());
}
}
private void restoreData(SQLiteDatabase 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");
ArrayList<String> properties = new ArrayList();
ArrayList<String> propertiesQuery = new ArrayList();
for (int j = 0; j < daoConfig.properties.length; j++) {
String columnName = daoConfig.properties[j].columnName;
if (getColumns(db, tempTableName).contains(columnName)) {
properties.add(columnName);
propertiesQuery.add(columnName);
} else {
try {
if (getTypeByClass(daoConfig.properties[j].type).equals("INTEGER")) {
propertiesQuery.add("0 as " + columnName);
properties.add(columnName);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
StringBuilder insertTableStringBuilder = new StringBuilder();
insertTableStringBuilder.append("INSERT INTO ").append(tableName).append(" (");
insertTableStringBuilder.append(TextUtils.join(",", properties));
insertTableStringBuilder.append(") SELECT ");
insertTableStringBuilder.append(TextUtils.join(",", propertiesQuery));
insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";");
StringBuilder dropTableStringBuilder = new StringBuilder();
dropTableStringBuilder.append("DROP TABLE ").append(tempTableName);
LogUtil.d("TAG", "插入正式表的SQL語句:" + insertTableStringBuilder.toString());
LogUtil.d("TAG", "銷毀臨時表的SQL語句:" + dropTableStringBuilder.toString());
db.execSQL(insertTableStringBuilder.toString());
db.execSQL(dropTableStringBuilder.toString());
}
}
private String getTypeByClass(Class<?> type) throws Exception {
if (type.equals(String.class)) {
return "TEXT";
}
if (type.equals(Long.class) || type.equals(Integer.class) || type.equals(long.class) || type.equals(int.class)) {
return "INTEGER";
}
if (type.equals(Boolean.class) || type.equals(boolean.class)) {
return "BOOLEAN";
}
Exception exception = new Exception(CONVERSION_CLASS_NOT_FOUND_EXCEPTION.concat(" - Class: ").concat(type.toString()));
exception.printStackTrace();
throw exception;
}
}
注意代碼沒有加鎖(其實這個太大必要),然后取用也十分簡單锌历,只需要到自己實現(xiàn)的繼承了DaoMaster.OpenHelper的實現(xiàn)類中的onUpgrade()方法中調用這句代碼即可
MigrationHelper.getInstance().migrate(db, PrivateMsgNoticeDao.class);
示例如下:
值得注意的是方法中第二個參數(shù)是一個可變參數(shù)贮庞,再這一個升級中可以填入多個我們需要升級數(shù)據(jù)表的實體類的Dao類,如:
MigrationHelper.getInstance().migrate(db, TestDao1.class, TestDao2.class);
這樣依賴我們可以一行代碼完成所有數(shù)據(jù)表的無損升級究西,是不是感覺非常方便窗慎,但是這里也有一個問題,就是上面提到的卤材,Greendao不支持默認數(shù)據(jù)遮斥,如果不賦值,那么在表中的字段都為null扇丛,也就是說我們升級之后的新表中從舊表轉移過去的數(shù)據(jù)新增的那個字段都為null术吗,更坑爹的是因為每次實體類都需要手動生成,所以我們也不太可能去實體類中設置默認值帆精,即使設置默認值最后也還是會為null较屿,因為從數(shù)據(jù)庫中讀取時會將null設置進該字段而覆蓋默認值,最后取到的還是null卓练,所以我們需要在接下來的調用中對該實體類對象字段的使用謹慎地判空隘蝎,我就被坑過,而且實在代碼被提交測試之后才發(fā)現(xiàn)的襟企,這也是我不推薦使用Greendao的原因之一嘱么,greendao的團隊太過傲嬌,居然連默認數(shù)據(jù)這么重要的API都不提供
2017-08-15更新
上面的方法在實際生產(chǎn)中被驗證可以解決問題顽悼,但存在缺陷:每次都要將需要保留的數(shù)據(jù)添加進migrate()方法中曼振,即使沒有升級數(shù)據(jù)表的Dao類,因為操作中會將添加進去的表備份然后刪除所有的表蔚龙,再將所有的備份過的表恢復冰评,長此以往要保留的數(shù)據(jù)表多了自然會有影響,且可能引發(fā)未知問題府蛇,替代解決方案請參考文尾--另外的解決方案
另外的解決辦法
我沒有試過這個方法集索,是我想過的方案之一,但沒有嘗試,思路是在數(shù)據(jù)庫升級時調用SQL語句為目標表動態(tài)創(chuàng)建一列數(shù)據(jù)务荆,為此我特意問過我們數(shù)據(jù)組的同事妆距,他告訴我是可以的,我查了一下SQL代碼如下(2017-08-15更新:經(jīng)過實際驗證方法可行)
alter table table_name add column (字段名 字段類型); ----此方法帶括號指定字段插入的位置:
實際代碼的方法封裝如下:
/**
* 升級數(shù)據(jù)庫時動態(tài)插入一列
*
* @param db 數(shù)據(jù)庫實體
* @param tabName 要操作的表名 如UserInfo
* @param columnName 要生成的列名 如UserId
* @param columnNameType 字段類型 如integer
*/
private static void insertColumn(SQLiteDatabase db, String tabName, String columnName, String columnNameType) {
db.execSQL("alter table \"" + tabName + "\" add column \"" + columnName + "\" " + columnNameType);
}
也可以在插入新字段時指定字段位置函匕,如插入于某字段之前娱据,SQL代碼如下
alter table table_name add column 字段名 字段類型 after 某字段;--這個方法就不知道要不要帶括號了
當然這個方法我也沒有嘗試是否有用盅惜,只是我的想法中剩,有需求或有興趣的同學也可以去試一下(2017-08-15更新:實測可用)
這些就是我遇到的問題以及解決辦法,也希望能幫到有需要的同學抒寂。