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方法,另外在指定(或預設)的包中生成DaoMaster、DaoSession以及表實體對應的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