因?yàn)镚reendao的高性能以及ORM框架的特點(diǎn)許多項(xiàng)目都是用了Greendao做為數(shù)據(jù)庫組件,不僅提升了開發(fā)效率并且使很多程序員擺脫了枯燥的SQL語句(包括我),這個(gè)框架也是非常受歡迎的,但是我在使用過程中也發(fā)現(xiàn)了這個(gè)框架的一些不足
- 不支持線程回調(diào)
- 不支持默認(rèn)數(shù)據(jù)類型
這兩點(diǎn)基本上是我使用這個(gè)ORM數(shù)據(jù)庫框架碰到的最失望的問題了,但是因?yàn)轫?xiàng)目之前使用的就是該框架且不已我的意志為轉(zhuǎn)移的情況下只能繼續(xù)使用,并且在我這一段時(shí)間的使用中對(duì)這個(gè)框架也有一些經(jīng)驗(yàn)和想法分享出來希望能幫到大家,我來說一下針對(duì)這兩個(gè)問題有些什么方法
線程回調(diào)問題
這個(gè)問題不是這篇文章的重點(diǎn),實(shí)際上我也沒有實(shí)際解決這個(gè)問題,但是有一些見解,如果是我們自己手動(dòng)為項(xiàng)目編寫數(shù)據(jù)庫組件的花一般都會(huì)遵循經(jīng)驗(yàn)講數(shù)據(jù)庫操作放在子線程中進(jìn)行并添加回調(diào),但是遺憾的是Greendao項(xiàng)目組可能出于對(duì)該組件效率的自信并沒有這么做(事實(shí)也證明Greendao確實(shí)是所有ORM框架中效率最高的),但是 我們還是有這個(gè)需求的,比如筆者的項(xiàng)目中就有需求是直接從服務(wù)器獲取上萬條數(shù)據(jù)然后插入到本地或者直接從本地獲取上萬條數(shù)據(jù)(此處只想吐槽產(chǎn)品經(jīng)理的腦回路),這個(gè)時(shí)候如果在UI線程插入或讀取雖然相對(duì)來講其實(shí)也還算快,但用戶總歸是會(huì)感覺到卡頓(大概一到兩秒,視實(shí)際機(jī)器性能不定),這總歸是很不爽的,且不被接受,存在ANR的風(fēng)險(xiǎn)

但是如果直接使用Greendao提供的異步方法我們又不知道何時(shí)插入完成,何時(shí)更新UI(這就很尷尬了),我們團(tuán)隊(duì)的解決方案是犧牲部分用戶體驗(yàn)后臺(tái)控制數(shù)據(jù)分批傳送,當(dāng)然,這其實(shí)是不得以的鴕鳥做法,正確的做法是:研究一下Greendao的源碼并添加回調(diào)接口,當(dāng)然這可能需要的不止一點(diǎn)時(shí)間,而且一般項(xiàng)目很少存在我這種一次性操作上萬條數(shù)據(jù)的情況,如果有,且在你們的項(xiàng)目還沒有上馬Greendao的情況下,趕緊棄暗投明 如果已經(jīng)上馬 趁項(xiàng)目崩潰前跑路吧

看到這里不要崩潰,開玩笑的,事情當(dāng)然沒有那么嚴(yán)重,問題是可以解決的,我們大可以在外部包裹線程實(shí)現(xiàn)GreenDao的異步調(diào)用,無論是AsyncTask還是自己實(shí)現(xiàn)一個(gè)線程異步類,查詢時(shí)開啟一個(gè)線程,查詢結(jié)果出來后再回調(diào)到主線程即可,工作量也不大,不過還是覺得GreenDao能提供的話還是要方便很多
當(dāng)然也可以向大家介紹一款國產(chǎn)ORM數(shù)據(jù)庫框架LitePay,由國內(nèi)Android開發(fā)大神郭霖開源,在最近最新的一版更新中該框架已支持異步回調(diào),當(dāng)然,對(duì)于Greendao的異步本文不再多講,畢竟本文最關(guān)心的另一個(gè)問題
數(shù)據(jù)庫升級(jí)問題
在項(xiàng)目中我們總會(huì)由各種各樣的問題需要對(duì)原有數(shù)據(jù)進(jìn)行升級(jí)例如增加新的字段,但是有的時(shí)候我們可能需又可能需要保留原來的數(shù)據(jù),在這里我不得不羨慕我的項(xiàng)目中負(fù)責(zé)另外的模塊的伙伴,他們的數(shù)據(jù)不僅能從服務(wù)器獲取且數(shù)據(jù)量極小,每次刪除后都可以從服務(wù)器重新獲取,根本不用擔(dān)心保留數(shù)據(jù)的問題,所以他們一般采用直接刪除舊表再創(chuàng)建新表的方法應(yīng)對(duì)數(shù)據(jù)庫版本升級(jí),有點(diǎn)小羨慕

但是 這種方式畢竟過于簡(jiǎn)單粗暴且不夠優(yōu)雅,最重要的是并不適合我的模塊,我一開始的思路是先取出數(shù)據(jù)保留再內(nèi)存中,然后將舊數(shù)據(jù)通過添加新字段默認(rèn)值的方式升級(jí)為新表適用的數(shù)據(jù),然后刪除舊表,創(chuàng)建新標(biāo),將轉(zhuǎn)換后的數(shù)據(jù)插入到新表中,但是不知道為什么這種方式看似沒毛病(肯定有毛病)但是每次都會(huì)報(bào)錯(cuò)(報(bào)錯(cuò)信息沒保留下來),不得已只能尋找另外別的方法,找來找去,發(fā)現(xiàn)有一種方法是通過升級(jí)時(shí)創(chuàng)建一個(gè)臨時(shí)表將數(shù)據(jù)保留下來,然后進(jìn)行表的升級(jí)再,再將數(shù)據(jù)轉(zhuǎn)移到新表中來實(shí)現(xiàn)的,這種方式和我的做法其實(shí)有點(diǎn)象,但可能考慮得比我多,不是將數(shù)據(jù)留在內(nèi)存中,而且經(jīng)過實(shí)測(cè)實(shí)際有用,我將代碼貼出來大家可以方便取用
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)建臨時(shí)表的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í)表插入數(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", "銷毀臨時(shí)表的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;
}
}
注意代碼沒有加鎖(其實(shí)這個(gè)太大必要),然后取用也十分簡(jiǎn)單,只需要到自己實(shí)現(xiàn)的繼承了DaoMaster.OpenHelper的實(shí)現(xiàn)類中的onUpgrade()方法中調(diào)用這句代碼即可
MigrationHelper.getInstance().migrate(db, PrivateMsgNoticeDao.class);
示例如下:

值得注意的是方法中第二個(gè)參數(shù)是一個(gè)可變參數(shù),再這一個(gè)升級(jí)中可以填入多個(gè)我們需要升級(jí)數(shù)據(jù)表的實(shí)體類的Dao類,如:
MigrationHelper.getInstance().migrate(db, TestDao1.class, TestDao2.class);
這樣依賴我們可以一行代碼完成所有數(shù)據(jù)表的無損升級(jí),是不是感覺非常方便,但是這里也有一個(gè)問題,就是上面提到的,Greendao不支持默認(rèn)數(shù)據(jù),如果不賦值,那么在表中的字段都為null,也就是說我們升級(jí)之后的新表中從舊表轉(zhuǎn)移過去的數(shù)據(jù)新增的那個(gè)字段都為null,更坑爹的是因?yàn)槊看螌?shí)體類都需要手動(dòng)生成,所以我們也不太可能去實(shí)體類中設(shè)置默認(rèn)值,即使設(shè)置默認(rèn)值最后也還是會(huì)為null,因?yàn)閺臄?shù)據(jù)庫中讀取時(shí)會(huì)將null設(shè)置進(jìn)該字段而覆蓋默認(rèn)值,最后取到的還是null,所以我們需要在接下來的調(diào)用中對(duì)該實(shí)體類對(duì)象字段的使用謹(jǐn)慎地判空,我就被坑過,而且實(shí)在代碼被提交測(cè)試之后才發(fā)現(xiàn)的,這也是我不推薦使用Greendao的原因之一,greendao的團(tuán)隊(duì)太過傲嬌,居然連默認(rèn)數(shù)據(jù)這么重要的API都不提供
2017-08-15更新
上面的方法在實(shí)際生產(chǎn)中被驗(yàn)證可以解決問題,但存在缺陷:每次都要將需要保留的數(shù)據(jù)添加進(jìn)migrate()方法中,即使沒有升級(jí)數(shù)據(jù)表的Dao類,因?yàn)椴僮髦袝?huì)將添加進(jìn)去的表備份然后刪除所有的表,再將所有的備份過的表恢復(fù),長此以往要保留的數(shù)據(jù)表多了自然會(huì)有影響,且可能引發(fā)未知問題,替代解決方案請(qǐng)參考文尾--另外的解決方案
另外的解決辦法
我沒有試過這個(gè)方法,是我想過的方案之一,但沒有嘗試,思路是在數(shù)據(jù)庫升級(jí)時(shí)調(diào)用SQL語句為目標(biāo)表動(dòng)態(tài)創(chuàng)建一列數(shù)據(jù),為此我特意問過我們數(shù)據(jù)組的同事,他告訴我是可以的,我查了一下SQL代碼如下(2017-08-15更新:經(jīng)過實(shí)際驗(yàn)證方法可行)
alter table table_name add column (字段名 字段類型); ----此方法帶括號(hào)指定字段插入的位置:
實(shí)際代碼的方法封裝如下:
/**
* 升級(jí)數(shù)據(jù)庫時(shí)動(dòng)態(tài)插入一列
*
* @param db 數(shù)據(jù)庫實(shí)體
* @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);
}
也可以在插入新字段時(shí)指定字段位置,如插入于某字段之前,SQL代碼如下
alter table table_name add column 字段名 字段類型 after 某字段;--這個(gè)方法就不知道要不要帶括號(hào)了
當(dāng)然這個(gè)方法我也沒有嘗試是否有用,只是我的想法,有需求或有興趣的同學(xué)也可以去試一下(2017-08-15更新:實(shí)測(cè)可用)
這些就是我遇到的問題以及解決辦法,也希望能幫到有需要的同學(xué)。