天天看點

如何優雅地使用GreenDao

前言

資料庫操作是Android開發中的重要部分,通常我們不直接使用SDK中的Sqlite API(難度大,開發效率低,當然運作效率是最快的),而是使用第三方的ORM架構,如 GreenDao。GreenDao可以極大地提高建庫,更新,增,删,改,查等工作的效率。雖然有GreenDao,如果不能很好地組織資料表和資料表的操作,随着業務的增加,依然會有很大的困擾。有沒有一個套路,寫起來代碼結構清晰,重複代碼少,友善擴充,調用起來也友善簡單?這就是本文要寫的内容,圍繞着這個問題的一次實踐。代碼的GitHub位址為:GreenDaoImpl

代碼結構

根據經驗,把資料表,資料表操作,資料庫更新等DB相關的代碼組織在一個包下可以更友善的查找與維護。把資料和資料的操作統一組織起來,作為資料源提供資料的存取,符合了子產品化設計的思想。是以,把GreenDao封裝在db包下,如圖所示:

如何優雅地使用GreenDao

db包下有四個類:AbstractDaoHandler,DaoManager,MyDataase,MyOpenHelper;五個二級包名:converter,dao,schema,typedef,upgrade。

  1. schema 顧名思義,可以了解為資料庫,這個包名下每一個類都代表一個資料表;
  2. dao ,這個包下的每一個類都繼承自 AbstractDaoHandler, 對應操作schema裡的一個資料表,資料表的所有操作都寫在XxxDaoProxy類(基本的增删改查在抽象類中實作了),内部的實作就是我們的GreenDao 生成的相應的 Dao類,每增加一個資料表,對應在dao下增加一個DaoProxy;
  3. typedef,這個包下是資料表中的自定義資料類型,與converter對應;
  4. converter,對應typedef,實作自定義的類型與基本類型互相轉化;
  5. upgrade,記錄每個版本變更;
  6. AbstractDaoHandler,資料表操作的基類,封裝了基本的增删改查操作,隻要繼承這個基類,傳遞一個資料表類型,無須添加任何代碼,即可擁有對資料表增删改查的能力;
  7. DaoManager 管理dao下的所有類,是一個單例類,DaoManager 對象擷取對應的XyzDaoProxy,每增加一個DaoProxy都在這裡增加一個DaoProxy對象,統一管理;
  8. MyDatabase 這裡建立Greendao的Database和DaoSession的執行個體,在Application中初始化後就可以擷取執行個體;
  9. MyOpenHelper 資料表更新,和upgrade中的類一起使用,管理每一個版本的變更。

根據類的關系,下面是大概的類圖:

如何優雅地使用GreenDao

這個代碼組織結構考慮了GreenDao提供的接口:建立實體,資料表操作,自定義類型,資料表更新。如果需要擴充新的資料表,或者增加新的表操作,隻需要橫向增加即可。資料庫,資料表定義,資料表接口,接口管理,資料更新一眼看去基本能了解個大概。在減少重複代碼的基礎上,增加了代碼的可閱讀性,可擴充性。

資料表結構定義

如何定義一張資料表,詳情請看 Modeling enties,這裡不做過多的描述,簡單記錄下需要注意的事項。

  1. 最理想的狀态是資料表結構一旦定義好,就不會發生變化(論開發初期定義好資料結構的重要性),實際随着版本更新,資料表結構可能會發生變化,需要改資料表的時候,更新 schemaVersion,記錄好哪個app版本對應哪個schemaVersion,有哪些變化,這個對後續app維護特别重要;
  2. 在設計資料表的時候,每個字段的類型,名稱,唯一性,能否為空等,要考慮清楚;
  3. 在設計資料表的時候,不要和背景 API 傳回的實體類型混為一體;
  4. Greendao 不支援聯合主鍵,但是可以通過 indexes 解決此問題,參考此文;
  5. 存儲自定義類型,可以通過 PropertyConverter 接口來實作;
  6. 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裡面,支援級聯删除等。