1. 程式人生 > >GreenDAO 完美解決資料庫升級資料丟失問題

GreenDAO 完美解決資料庫升級資料丟失問題

前言

在以前選擇資料庫框架的時候,接觸過GreenDAO,但由於那時的GreenDAO配置起來很繁瑣,需要自己建立java庫,所以就沒使用它。 
但如今在3.0版本後,GreenDAO大大簡化了使用流程,加上其本身存取快、體積小、支援快取、支援加密等優點,使得它成為了一個更受歡迎的ORM解決方案。

附上一張官方提供的,GreenDAO、OrmLite、ActiveAndroid的對比圖

對比圖

介紹

下面分為 配置、建庫建表、增刪改查、加密、升級、混淆 這幾個部分來介紹。

1. 配置

1.1 新增依賴

Project下的build.gradle檔案加入

dependencies {
    //greendao配置
classpath 'org.greenrobot:greendao-gradle-plugin:3.2.0' }
  • 1
  • 2
  • 3
  • 4

Module下的build.gradle檔案加入

apply plugin: 'org.greenrobot.greendao'

dependencies {
    //greenDAO配置
    compile 'org.greenrobot:greendao:3.2.0'
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

1.2 設定版本號、生成路徑

android {
    //greendao配置
    greendao {
        //資料庫版本號,升級時修改
        schemaVersion 1
//生成的DAO,DaoMaster和DaoSession的包路徑。預設與表實體所在的包路徑相同 daoPackage 'com.dev.base.model.db' //生成原始檔的路徑。預設原始檔目錄是在build目錄中的(build/generated/source/greendao) targetGenDir 'src/main/java' } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

2. 建庫建表

2.1 建立資料庫

//DaoMaster為後面建立表實體後自動生成的類。
DaoMaster.DevOpenHelper helper = new
DaoMaster.DevOpenHelper(this, "test.db", null);
  • 1
  • 2

2.2 建立資料表

GreenDAO通過ORM(Object Relation Mapping 物件關係對映)的方式建立資料表。即建立一個實體類,將該實體結構對映成資料表的結構。 
下面演示如何建立一個”電影收藏”資料表。分兩步:建立表實體,Make Project生成程式碼。

2.2.1 建立表實體

@Entity
public class MovieCollect {

    @Id
    private Long id;
    private String movieImage;
    private String title;
    private int year;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

@Entity 
用來宣告類實體,表示它將對映為資料表 
@Entity()括號內可加入更詳細的設定,如: 
nameInDb =“TABLE_NAME” ——> 宣告該表的表名,預設取類名 
createInDb = true ——> 是否建立表,預設為true 
generateConstructors = true ——> 是否生成含所有引數的建構函式,預設為true 
generateGettersSetters = true ——> 是否生成getter/setter,預設為true

@Id 
用來宣告某變數為表的主鍵,型別使用Long 
@Id()括號可加入autoincrement = true表明自增長

@Unique 
用來宣告某變數的值需為唯一值

@NotNull 
用來宣告某變數的值不能為null

@Property 
@Property(nameInDb = “URL”) 用來宣告某變數在表中的實際欄位名為URL

@Transient 
用來宣告某變數不被對映到資料表中

@ToOne、@ToMany 
用來宣告”對一”和“對多”關係,下面舉例說明:

  • 學生與學校之間一對多的關係(一個學生對應一個學校,一個學校對應有多個學生)
@Entity
class Student{
    //...省略其他變數
    private long fk_schoolId;//外來鍵

    @ToOne(joinProperty = "fk_schoolId")
    private School school;
}

@Entity
class School{
    //...省略其他變數 
    @ToMany(referencedJoinProperty = "fk_schoolId")
    private List<Student> students;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 學生與課程之間“多對多”的關係(一個學生對應有多門課程,一門課程對應有多個學生)
@Entity
class Student{
    //...省略其他變數
    @ToMany
    @JoinEntity(
            entity = StudentWithCourse.class,
            sourceProperty = "sId",
            targetProperty = "cId"
    )
    private List<Course> courses;
}

@Entity 
class Course{
    //...省略其他變數
    @ToMany
    @JoinEntity(
            entity = StudentWithCourse.class,
            sourceProperty = "cId",
            targetProperty = "sId"
    )
    private List<Course> courses;
}

@Entity
class StudentWithCourse{
    @Id
    private Long id;
    private Long sId;
    private Long cId;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

2.2.2 Make Project

利用上面註解寫好表實體後,通過Build—>Make Project重新編譯專案, 將會在表實體中自動生成構造方法getter/setter方法,另外在指定(或預設)的包中生成DaoMasterDaoSession以及表實體對應的Dao(如MovieCollectDao)。

其中, 
DaoMaster:用於建立資料庫以及獲取DaoSession 
DaoSession:用於獲取各個表對應的Dao類 
各個表對應的Dao:提供了對錶進行增刪改查的方法

3. 增刪改查

要對資料表進行增刪改查,需要獲取該表對應的Dao類,而獲取Dao類需要DaoSession。 
建立一個DaoManager用於初始化資料庫以及提供DaoSession。

//DaoManager中程式碼

//獲取DaoSession,從而獲取各個表的操作DAO類
public DaoSession getDaoSession() {
    if (mDaoSession == null) {
        initDataBase();
    }
    return mDaoSession;
}

//初始化資料庫及相關類
private void initDataBase(){
    setDebugMode(true);//預設開啟Log列印
    mSQLiteOpenHelper = new DaoMaster.DevOpenHelper(MyApplication.getInstance(), DB_NAME, null);//建庫
    mDaoMaster = new DaoMaster(mSQLiteOpenHelper.getWritableDatabase());
    mDaoSession = mDaoMaster.newSession();
    mDaoSession.clear();//清空所有資料表的快取
}

//是否開啟Log
public void setDebugMode(boolean flag) {
    MigrationHelper.DEBUG = true;//如果檢視資料庫更新的Log,請設定為true
    QueryBuilder.LOG_SQL = flag;
    QueryBuilder.LOG_VALUES = flag;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

通過DaoSeesion獲取目標資料表對應的Dao,然後就可開始進行增刪改查的操作了。

MovieCollectDao mMovieCollectDao = DaoManager.getInstance().getDaoSession().getMovieCollectDao();
  • 1

3.1 增

  • 插入單個數據
MovieCollect movieCollect;
mMovieCollectDao.insert(movieCollect);
  • 1
  • 2
  • 插入一組資料
List<MovieCollect> listMovieCollect;
mMovieCollectDao.insertInTx(listMovieCollect);
  • 1
  • 2
  • 插入或替換資料
//插入的資料如果已經存在表中,則替換掉舊資料(根據主鍵來檢測是否已經存在)

MovieCollect movieCollect;
mMovieCollectDao.insertOrReplace(movieCollect);//單個數據

List<MovieCollect> listMovieCollect;
mMovieCollectDao.insertOrReplace(listMovieCollect);//一組資料
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.2 刪

  • 刪除單個數據
MovieCollect movieCollect;
mMovieCollectDao.delete(movieCollect);
  • 1
  • 2
  • 刪除一組資料
List<MovieCollect> listMovieCollect;
mMovieCollectDao.deleteInTx(listMovieCollect);
  • 1
  • 2
  • 刪除所有資料
mMovieCollectDao.deleteAll();
  • 1

3.3 改

  • 修改單個數據
MovieCollect movieCollect;
mMovieCollectDao.update(movieCollect);
  • 1
  • 2
  • 修改一組資料
List<MovieCollect> listMovieCollect;
mMovieCollectDao.updateInTx(listMovieCollect);
  • 1
  • 2

3.4 查

  • 查詢全部資料
List<MovieCollect> listMovieCollect = mMovieCollectDao.loadAll();
  • 1
  • 查詢數量
int count = mMovieCollectDao.count();
  • 1
  • 條件查詢

精確查詢(where)

//查詢電影名為“肖申克的救贖”的電影
MovieCollect movieCollect = 
mMovieCollectDao.queryBuilder().where(MovieCollectDao.Properties.Title.eq("肖申克的救贖")).unique(); 

//查詢電影年份為2017的電影
List<MovieCollect> movieCollect =
mMovieCollectDao.queryBuilder().where(MovieCollectDao.Properties.Year.eq(2017)).list(); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

模糊查詢(like)

//查詢電影名含有“傳奇”的電影
List<MovieCollect> movieCollect = mMovieCollectDao.queryBuilder().where(MovieCollectDao.Properties.Title.like("傳奇")).list();

//查詢電影名以“我的”開頭的電影
List<MovieCollect> movieCollect = mMovieCollectDao.queryBuilder().where(MovieCollectDao.Properties.Title.like("我的%")).list();
  • 1
  • 2
  • 3
  • 4
  • 5

區間查詢

//大於
//查詢電影年份大於2012年的電影
List<MovieCollect> movieCollect = mMovieCollectDao.queryBuilder().where(MovieCollectDao.Properties.Year.gt(2012)).list();

//大於等於
//查詢電影年份大於等於2012年的電影
List<MovieCollect> movieCollect = mMovieCollectDao.queryBuilder().where(MovieCollectDao.Properties.Year.ge(2012)).list();

//小於
//查詢電影年份小於2012年的電影
List<MovieCollect> movieCollect = mMovieCollectDao.queryBuilder().where(MovieCollectDao.Properties.Year.lt(2012)).list();

//小於等於
//查詢電影年份小於等於2012年的電影
List<MovieCollect> movieCollect = mMovieCollectDao.queryBuilder().where(MovieCollectDao.Properties.Year.le(2012)).list();

//介於中間
//查詢電影年份在2012-2017之間的電影
List<MovieCollect> movieCollect = mMovieCollectDao.queryBuilder().where(MovieCollectDao.Properties.Year.between(2012,2017)).list();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

升序降序

//查詢電影年份大於2012年的電影,並按年份升序排序
List<MovieCollect> movieCollect = mMovieCollectDao.queryBuilder().where(MovieCollectDao.Properties.Year.gt(2012)).orderAsc(MovieCollectDao.Properties.Year).list();

//查詢電影年份大於2012年的電影,並按年份降序排序
List<MovieCollect> movieCollect = mMovieCollectDao.queryBuilder().where(MovieCollectDao.Properties.Year.gt(2012)).orderDesc(MovieCollectDao.Properties.Year).list();
  • 1
  • 2
  • 3
  • 4
  • 5

and/or

//and
//查詢電影年份大於2012年且電影名以“我的”開頭的電影
List<MovieCollect> movieCollect = mMovieCollectDao.queryBuilder().and(MovieCollectDao.Properties.Year.gt(2012), MovieCollectDao.Properties.Title.like("我的%")).list();

//or
//查詢電影年份小於2012年或者大於2015年的電影
List<MovieCollect> movieCollect = mMovieCollectDao.queryBuilder().or(MovieCollectDao.Properties.Year.lt(2012), MovieCollectDao.Properties.Year.gt(2015)).list();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

SQL語句

//查詢名字為“羞羞的鐵拳”的電影

//使用Dao.queryBuilder().where() 配合 WhereCondition.StringCondition() 實現SQL查詢
Query query =
mMovieCollectDao.queryBuilder()
.where(new WhereCondition.StringCondition("TITLE = ?", "羞羞的鐵拳")).build();
List<MovieCollect> movieCollect = query.list();

//使用Dao.queryRawCreate() 實現SQL查詢
Query query = mMovieCollectDao.queryRawCreate("WHERE TITLE = ?", "羞羞的鐵拳");
List<MovieCollect> movieCollect = query.list();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 快取問題

由於GreenDao預設開啟了快取,所以當你呼叫A查詢語句取得X實體,然後對X實體進行修改並更新到資料庫,接著再呼叫A查詢語句取得X實體,會發現X實體的內容依舊是修改前的。其實你的修改已經更新到資料庫中,只是查詢採用了快取,所以直接返回了第一次查詢的實體。 
解決方法:查詢前先清空快取,清空方法如下

//清空所有資料表的快取資料
DaoSession daoSession = DaoManager.getInstance().getDaoSession();
daoSession .clear();

//清空某個資料表的快取資料
MovieCollectDao movieCollectDao = DaoManager.getInstance().getDaoSession().getMovieCollectDao();
movieCollectDao.detachAll();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

4. 加密

注意: 加密需要新增sqlcipher庫,而該庫體積龐大,使用後apk大小會增加大概5M,所以如果你的應用對安全性要求不高,不建議使用。

加密資料庫的步驟: 
第一步: 
Module下的build.gradle中新增一個庫依賴,用於資料庫加密。

compile 'net.zetetic:android-database-sqlcipher:3.5.7'//使用加密資料庫時需要新增
  • 1

第二步: 
獲取DaoSession的過程中,使用getEncryptedWritableDb(“你的密碼”)來獲取操作的資料庫,而不是getWritableDatabase()。

mSQLiteOpenHelper = new MySQLiteOpenHelper(MyApplication.getInstance(), DB_NAME, null);//建庫
mDaoMaster = new DaoMaster(mSQLiteOpenHelper.getEncryptedWritableDb("你的密碼"));//加密
//mDaoMaster = new DaoMaster(mSQLiteOpenHelper.getWritableDatabase());
mDaoSession = mDaoMaster.newSession();
  • 1
  • 2
  • 3
  • 4

第三步: 
使用上面步驟得到的DaoSession進行具體的資料表操作。 
如果執行後報無法載入有關so庫的異常,請對專案進行clean和rebuild。

5. 升級

在版本迭代時,我們經常需要對資料庫進行升級,而GreenDAO預設的DaoMaster.DevOpenHelper在進行資料升級時,會把舊錶刪除,然後建立新表,並沒有遷移舊資料到新表中,從而造成資料丟失。 
這在實際中是不可取的,因此我們需要作出調整。下面介紹資料庫升級的步驟與要點。

第一步: 
新建一個類,繼承DaoMaster.DevOpenHelper,重寫onUpgrade(Database db, int oldVersion, int newVersion)方法,在該方法中使用MigrationHelper進行資料庫升級以及資料遷移。 
網上有不少MigrationHelper的原始碼,這裡採用的是https://github.com/yuweiguocn/GreenDaoUpgradeHelper中的MigrationHelper,它主要是通過建立一個臨時表,將舊錶的資料遷移到新表中,大家可以去看下原始碼。

public class MyOpenHelper extends DaoMaster.OpenHelper {
    public MyOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
        super(context, name, factory);
    }

    @Override
    public void onUpgrade(Database db, int oldVersion, int newVersion) {

        //把需要管理的資料庫表DAO作為最後一個引數傳入到方法中
        MigrationHelper.migrate(db, new MigrationHelper.ReCreateAllTableListener() {

            @Override
            public void onCreateAllTables(Database db, boolean ifNotExists) {
                DaoMaster.createAllTables(db, ifNotExists);
            }

            @Override
            public void onDropAllTables(Database db, boolean ifExists) {
                DaoMaster.dropAllTables(db, ifExists);
            }
        },  MovieCollectDao.class);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

然後使用MyOpenHelper替代DaoMaster.DevOpenHelper來進行建立資料庫等操作

mSQLiteOpenHelper = new MyOpenHelper(MyApplication.getInstance(), DB_NAME, null);//建庫
mDaoMaster = new DaoMaster(mSQLiteOpenHelper.getWritableDatabase());
mDaoSession = mDaoMaster.newSession();
  • 1
  • 2
  • 3

第二步: 
在表實體中,調整其中的變數(表字段),一般就是新增/刪除/修改欄位。注意: 
1)新增的欄位或修改的欄位,其變數型別應使用基礎資料型別的包裝類,如使用Integer而不是int,避免升級過程中報錯。 
2)根據MigrationHelper中的程式碼,升級後,新增的欄位和修改的欄位,都會預設被賦予null值。

第三步: 
將原本自動生成的構造方法以及getter/setter方法刪除,重新Build—>Make Project進行生成。

第四步: 
修改Module下build.gradle中資料庫的版本號schemaVersion ,遞增加1即可,最後執行app

greendao {
    //資料庫版本號,升級時進行修改
    schemaVersion 2

    daoPackage 'com.dev.base.model.db'
    targetGenDir 'src/main/java'
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

6. 混淆

在proguard-rules.pro檔案中新增以下內容進行混淆配置

# greenDAO開始
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
public static java.lang.String TABLENAME;
}
-keep class **$Properties
# If you do not use SQLCipher:
-dontwarn org.greenrobot.greendao.database.**
# If you do not use RxJava:
-dontwarn rx.**
# greenDAO結束


# 如果按照上面介紹的加入了資料庫加密功能,則需新增一下配置
#sqlcipher資料庫加密開始
-keep  class net.sqlcipher.** {*;}
-keep  class net.sqlcipher.database.** {*;}
#sqlcipher資料庫加密結束
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17