前言
資料庫操作是Android開發中的重要部分,通常我們不直接使用SDK中的Sqlite API(難度大,開發效率低,當然運作效率是最快的),而是使用第三方的ORM架構,如 GreenDao。GreenDao可以極大地提高建庫,更新,增,删,改,查等工作的效率。雖然有GreenDao,如果不能很好地組織資料表和資料表的操作,随着業務的增加,依然會有很大的困擾。有沒有一個套路,寫起來代碼結構清晰,重複代碼少,友善擴充,調用起來也友善簡單?這就是本文要寫的内容,圍繞着這個問題的一次實踐。代碼的GitHub位址為:GreenDaoImpl
代碼結構
根據經驗,把資料表,資料表操作,資料庫更新等DB相關的代碼組織在一個包下可以更友善的查找與維護。把資料和資料的操作統一組織起來,作為資料源提供資料的存取,符合了子產品化設計的思想。是以,把GreenDao封裝在db包下,如圖所示:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHLsh3RkBTOtllZadkYohmMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL3YDO3ATMzIjM4ATMwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
db包下有四個類:AbstractDaoHandler,DaoManager,MyDataase,MyOpenHelper;五個二級包名:converter,dao,schema,typedef,upgrade。
- schema 顧名思義,可以了解為資料庫,這個包名下每一個類都代表一個資料表;
- dao ,這個包下的每一個類都繼承自 AbstractDaoHandler, 對應操作schema裡的一個資料表,資料表的所有操作都寫在XxxDaoProxy類(基本的增删改查在抽象類中實作了),内部的實作就是我們的GreenDao 生成的相應的 Dao類,每增加一個資料表,對應在dao下增加一個DaoProxy;
- typedef,這個包下是資料表中的自定義資料類型,與converter對應;
- converter,對應typedef,實作自定義的類型與基本類型互相轉化;
- upgrade,記錄每個版本變更;
- AbstractDaoHandler,資料表操作的基類,封裝了基本的增删改查操作,隻要繼承這個基類,傳遞一個資料表類型,無須添加任何代碼,即可擁有對資料表增删改查的能力;
- DaoManager 管理dao下的所有類,是一個單例類,DaoManager 對象擷取對應的XyzDaoProxy,每增加一個DaoProxy都在這裡增加一個DaoProxy對象,統一管理;
- MyDatabase 這裡建立Greendao的Database和DaoSession的執行個體,在Application中初始化後就可以擷取執行個體;
- MyOpenHelper 資料表更新,和upgrade中的類一起使用,管理每一個版本的變更。
根據類的關系,下面是大概的類圖:
這個代碼組織結構考慮了GreenDao提供的接口:建立實體,資料表操作,自定義類型,資料表更新。如果需要擴充新的資料表,或者增加新的表操作,隻需要橫向增加即可。資料庫,資料表定義,資料表接口,接口管理,資料更新一眼看去基本能了解個大概。在減少重複代碼的基礎上,增加了代碼的可閱讀性,可擴充性。
資料表結構定義
如何定義一張資料表,詳情請看 Modeling enties,這裡不做過多的描述,簡單記錄下需要注意的事項。
- 最理想的狀态是資料表結構一旦定義好,就不會發生變化(論開發初期定義好資料結構的重要性),實際随着版本更新,資料表結構可能會發生變化,需要改資料表的時候,更新 schemaVersion,記錄好哪個app版本對應哪個schemaVersion,有哪些變化,這個對後續app維護特别重要;
- 在設計資料表的時候,每個字段的類型,名稱,唯一性,能否為空等,要考慮清楚;
- 在設計資料表的時候,不要和背景 API 傳回的實體類型混為一體;
- Greendao 不支援聯合主鍵,但是可以通過 indexes 解決此問題,參考此文;
- 存儲自定義類型,可以通過 PropertyConverter 接口來實作;
- GreenDao支援一對一,一對多的關系,但是并不支援級聯删除
如果要增加一張新資料表Xyz,在schema包下建立一個Xyz類,定義其表結構。然後在dao包中增加一個XyzDaoProxy類,繼承自AbstractDaoHandler,最後注冊在DaoManager。這樣就可以使用就可以通過DaoManager擷取到這個XyzDaoProxy,然後就可以對Xyz這個資料表進行增删改查等操作了。
Dao封裝
檢視GreenDao生成的代碼中,有一個抽象類:AbstractDao<T, K>,這個類封裝了資料表的操作。每一個對應的Dao類,都繼承自這個類。是以,可以利用這個類,建立我們的AbstractDaoHandler,其中T就是對應的實體類,然後AbstractDaoHandler通過構造函數,注入一個AbstractDao<T, Long> 對象,這個對象就是GreenDao生成的XyzDao。
import org.greenrobot.greendao.AbstractDao;
import org.greenrobot.greendao.query.QueryBuilder;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractDaoHandler<T>{
protected AbstractDao<T, Long> dao;
public AbstractDaoHandler(AbstractDao<T, Long> dao) {
this.dao = dao;
}
public long insert(T data) {
return dao.insert(data);
}
public void insertInTx(List<T> data) {
dao.insertInTx(data);
}
public void update(T data) {
dao.update(data);
}
public void updateInTx(List<T> data) {
dao.updateInTx(data);
}
public long insertOrReplace(T data) {
return dao.insertOrReplace(data);
}
public void insertOrReplaceInTx(List<T> data) {
dao.insertOrReplaceInTx(data);
}
public T loadByRowId(Long id) {
return dao.loadByRowId(id);
}
public List<T> loadAll() {
return dao.loadAll();
}
public void deleteByKey(long id) {
dao.deleteByKey(id);
}
public void delete(T data) {
dao.delete(data);
}
public void deleteInTx(List<T> data) {
dao.deleteInTx(data);
}
public void deleteAll() {
dao.deleteAll();
}
public QueryBuilder<T> queryBuilder() {
return dao.queryBuilder();
}
public List<T> query(QueryBuilder<T> queryBuilder) {
List<T> result = null;
try {
result = queryBuilder.list();
} catch (Exception e) {
e.printStackTrace();
}
if(result == null) {
result = new ArrayList<>();
}
return result;
}
public T unique(QueryBuilder<T> queryBuilder) {
T result = null;
try {
result = queryBuilder.unique();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
public abstract T unique(T data);
}
資料庫更新
資料庫更新是一件不可避免的事情。關于資料庫更新,有兩個事實,一,隻能從低版本更新到高版本,而不能從高版本降級;二,由于app版本更新的原因,可能會出現從版本1直接更新到版本3,而跳過版本2,如果不提供1到3的直接更新,隻能從1更新到2,然後再從2更新到3。我們記錄每一個版本的變更,然後根據oldVersion可以判斷需要執行哪幾個版本的更新,如oldVersion=3,但是newVersion=5,那麼有3-4,4-5兩個版本要更新。定義一個抽象基類,一個抽象方法upgrade(),每個版本更新,就繼承自這個類,并實作upgrde():
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;
import android.util.Log;
/**
* 通過比較versionCode與oldVersion,如果 oldVersion <= versionCode,則需要更新。
*/
public abstract class Migration {
/**
* 這個版本号可以了解是上一個版本,如目前将要釋出的版本是2,上一個版本是1,則versionCode=1;
* 以此類推,versionCode=1,2,3...(假定versionCode每次更新都自增1)
*/
public final int versionCode;
public Migration(int versionCode) {
this.versionCode = versionCode;
}
public void migrate(SQLiteDatabase db, int oldVersion) {
if(oldVersion <= versionCode) {
upgrade(db);
}
}
protected String addColumn(String tableName, String columnName, String type, boolean nullAble, String defaultVal) {
StringBuilder addColumn = new StringBuilder();
addColumn.append("ALTER TABLE ").append(tableName)
.append(" ADD COLUMN ").append(columnName).append(" ").append(type);
if(!nullAble) {
addColumn.append(" NOT NULL DEFAULT(").append(defaultVal).append(");");
} else {
addColumn.append(";");
}
String sql = addColumn.toString();
Log.d("MyOpenHelper", sql);
return sql;
}
protected void alterColumnNullAble(String tableName, String columnName, String type, boolean nullAble, String defaultVal) {
// ALTER TABLE 表 ALTER COLUMN [字段名] 字段類型 NOT NULL
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("ALTER TABLE ").append(tableName)
.append(" ALTER COLUMN ").append(columnName).append(" ").append(type);
if(nullAble) {
stringBuilder.append(" NULL;");
} else {
stringBuilder.append(" NOT NULL DEFAULT(").append(defaultVal).append(");");
}
}
protected boolean hasColumn(SQLiteDatabase db, String tableName, String column) {
if (TextUtils.isEmpty(tableName) || TextUtils.isEmpty(column)) {
return false;
}
Cursor cursor = null;
try {
cursor = db.query(tableName, null, null,
null, null, null, null);
if (null != cursor && cursor.getColumnIndex(column) != -1) {
return true;
}
} finally {
if (null != cursor) {
cursor.close();
}
}
return false;
}
protected abstract void upgrade(SQLiteDatabase db);
}
記錄了每個版本的變更,需要在MyOpenHelper 中調用更新,注意更新時有順序的,必須保證從低到高,逐個版本加入(Migration2,MIgration3…):
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import com.bottle.alive.db.upgrade.Migration;
import com.bottle.alive.gen.DaoMaster;
import org.greenrobot.greendao.database.Database;
import org.greenrobot.greendao.database.StandardDatabase;
import java.util.ArrayList;
import java.util.List;
/**
* 資料表更新,配合app/build.gradle 中的 greendao#schemaVersion使用,更新資料庫中的資料表(如增加column等操作)
* 建議:1.開始設計時要盡量考慮未來的需求,避免改動;
* 2.如果要修改表,盡量新增column,而不要删除column,也不要修改column的類别
* 3.沒必要遷移整張表的資料,考慮使用SQL 新增column或者修改column
* 4.版本名稱從1開始,每次更新增加1,并且記錄每個版本的變化
*/
public class MyOpenHelper extends DaoMaster.OpenHelper {
private List<Migration> migrations;
public MyOpenHelper(Context context, String name) {
super(context, name);
}
public MyOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
super(context, name, factory);
}
@Override
public void onCreate(Database db) {
super.onCreate(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
super.onUpgrade(db, oldVersion, newVersion);
if(migrations == null) {
// 如果沒有提供更新的對象,那麼删除所有的表,然後再重建立表
DaoMaster.dropAllTables( new StandardDatabase(db), true);
onCreate(db);
} else {
for (Migration migration : migrations) {
migration.migrate(db, oldVersion);
}
}
}
/**
* 按照從低到高的版本順序排列
* @param migrations 每更新一個版本就建立一個Migration類
*/
public void setMigrations(Migration... migrations) {
if(migrations == null || migrations.length == 0) {
return;
}
if(this.migrations == null) {
this.migrations = new ArrayList<>();
}
this.migrations.clear();
for(Migration migration : migrations) {
this.migrations.add(migration);
}
}
}
關于更新,剛開始搜尋到一個項目GreenDaoUpgradeHelper,它主要是通過建立一個臨時表,将舊表的資料遷移到新表。但是覺得沒必要建立臨時表并遷移資料,然後删除老表,再建立,最後将臨時表中的資料寫入到新表。通過執行一個SQL,直接對資料表進行修改,可以避免這麼複雜的流程。是以,沒有嘗試使用這個項目的代碼。
總結
GreenDao 可以大大提高SQLite資料的開發效率。本文總結了GreenDao建立資料表,封裝資料表操作,資料表更新的一些實戰經驗,進而更好地掌控App的資料模型。最近在看Android的Jetpack,有一個類似的元件Room,感覺可以替代GreenDao。因為它支援很多GreenDao不支援的特性,如Room支援把代碼單獨寫在一個Module裡面,支援級聯删除等。