GreenDao<第六篇>:GreenDao3.0全面分析

這已經是第六篇了,本來還打算分好幾個章節寫的,但是考慮到可讀性,所以本章將全面講解GreenDao的使用。

數據庫的加密前面篇章已經寫的,本章就不介紹了。

在Android開發中,我們需要選擇一個數據庫管理工具,Android支持直接使用SQL語句,但是如果直接使用SQL語句的話顯然出錯率比較高。GreenDao是現在主流的數據庫框架之一。

(1)作用

通過GreenDao,我們可以更快速的操作數據庫,我們可以使用簡單的面相對象的API來存儲,更新,刪除和查詢Java對象。(CUID)

(2)優缺點
  • 堅如磐石:greenDAO自2011年以來一直存在,並被無數着名的應用程序使用
  • 超級簡單:簡潔直接的API,帶有註釋的V3
  • 小:庫是<150K,它只是普通的Java jar(沒有CPU依賴的本機部分)
  • 快速:可能是Android上最快的ORM,由智能代碼生成驅動
  • 安全和富有表現力的查詢API:QueryBuilder使用屬性常量來避免拼寫錯誤
  • 強大的連接:跨實體查詢,甚至是複雜關係的鏈接
  • 靈活的屬性類型:使用自定義類或枚舉來表示實體中的數據
  • 加密:支持SQLCipher加密數據庫
(3)配置

GreenDao<第四篇>:GreenDao3的配置

(4)核心類

GreenDao的核心類有三個:分別是DaoMaster,DaoSession,XXXDao,這三個類都是自動生成的。(第三點配置有說明)

DaoMaster: DaoMaster保存數據庫對象(SQLiteDatabase)並管理特定模式的DAO類(而不是對象)。它有靜態方法來創建表或刪除它們。它的內部類OpenHelper和DevOpenHelper是SQLiteOpenHelper實現,它們在SQLite數據庫中創建模式。
DaoSession: 管理特定模式的所有可用DAO對象,您可以使用其中一個getter方法獲取該對象。DaoSession還提供了一些通用的持久性方法,如實體的插入,加載,更新,刷新和刪除。
XXXDao: 數據訪問對象(DAO)持久存在並查詢實體。對於每個實體,greenDAO生成DAO。它具有比DaoSession更多的持久性方法,例如:count,loadAll和insertInTx。
Entities : 可持久化對象。通常, 實體對象代表一個數據庫行使用標準 Java 屬性(如一個POJO 或 JavaBean )。

(5) GreenDao初始化

GreenDao<第五篇>:初始化數據庫

這個之前已經提到過,說白了就是獲取指定數據庫,如果沒有數據庫則新建一個數據庫,GreenDao初始化代碼一般放在Application中執行,因爲我們的最終目的是要拿到DaoSession,DaoSession可以管理各種Dao。

(7)實體註解@Entity

@Entity是Greendao註解之一,Greendao只保留用此註釋標記的類的對象,用次註解標註的類的對象就是一個實體

我們查看一下源碼,發現該註解有一些成員變量:

nameInDb: 指定此實體映射到的數據庫端的名稱(例如表名)。默認情況下,名稱基於實體類名稱,默認爲空。
indexes: 實體的索引,默認爲空。
createInDb: 是否允許在數據庫中創建表格,默認爲true。
schema: 指定實體的架構名稱:Greendao可以爲每個架構生成獨立的類集。屬於不同模式的實體應該有關係。默認爲“default”。
active: 標記一個實體處於活躍狀態,活動實體有更新、刪除和刷新方法。默認爲false。
generateConstructors: 是否生成構造方法。默認爲true。
generateGettersSetters: 如果缺少setXXX或者getXXX,是否生成setXXX或者getXXX。默認爲true。
protobuf: 定義此實體的protobuf類以爲其創建額外的特殊DAO。默認爲void.class。

接下來,我們一起看一下其用法吧。

  • @Entity的用法

簡單聲明一個實體,最簡單的代碼如下:

@Entity
public class Student {
    private String mId;
    private String mName;
    private int mGender;
}

也就是說,被註解@Entity修飾的類就是一個實體了,重新編譯之後就會自動生成DaoMaster.javaDaoSession.javaStudentDao.java這是三個類。

  • nameInDb的用法

nameInDb的默認值是“”,所以其表明如圖所示:

默認表名就是類的大寫字母,非駝峯命名法。

nameInDb屬性的配置,可以將表格名稱的命名不再默認,如:

@Entity(nameInDb = "T_STUDENT")
public class Student {
    private String mId;
    private String mName;
    private int mGender;
}

值得注意的是,表格的命名最好是字母和下劃線,如果存在特殊符號,比如:

@Entity(nameInDb = "t_student、")
public class Student {
    private String mId;
    private String mName;
    private int mGender;
}

上圖可知,表名亂碼了,所以爲了避免這種情況,nameInDb的取值最好是字母和下劃線,或者加個數字也是可以的。

還有一點需要注意,假如本地已經存在STUDENT表,如果修改屬性nameInDb = "T_STUDENT",那麼STUDENT表的表名不會被修改爲T_STUDENT,只有在第一次建表的時候纔會命名爲T_STUDENT。

  • indexes的使用

indexes顧名思義就是索引的意思。

索引是對數據庫表中一列或多列的值進行排序的一種結構,使用索引可快速訪問數據庫表中的特定信息。如果想按特定職員的姓來查找他或她,則與在表中搜索所有的行相比,索引有助於更快地獲取信息。

索引的一個主要目的就是加快檢索表中數據,亦即能協助信息搜索者儘快的找到符合限制條件的記錄ID的輔助數據結構。

代碼如下:

@Entity(
        nameInDb = "T_STUDENT",
        indexes = {
                @Index(name = "index1", value = "mName", unique = true)
        }

)
public class Student {
    @Id(autoincrement = true)
    private Long mId;
    private String mName;
    private int mGender;

    //....
}

之前介紹了nameInDb屬性,如果多個屬性就用逗號隔開。

以上代碼就是索引集合的簡單寫法,name是索引名稱,value是表的字段名稱,unique是索引的唯一約束。

【例一】創建常規索引

    indexes = {
            @Index(name = "index1", value = "mName", unique = false)
    }

只要將unique的值改成false即可。

【例二】創建唯一索引

    indexes = {
            @Index(name = "index1", value = "mName", unique = true)
    }

只要將unique的值改成true即可。

【例三】創建一個按字段正序排序或者倒序排序的索引

正序

indexes = {
        @Index(name = "index1", value = "mName asc", unique = true)
}

倒序

indexes = {
        @Index(name = "index1", value = "mName desc", unique = true)
}

【例四】一個索引綁定多個字段

indexes = {
        @Index(name = "index1", value = "mId desc, mName asc", unique = true)
}

字段和字段之間用逗號隔開。

【例五】創建多個索引

indexes = {
        @Index(name = "index1", value = "mId, mName asc", unique = true),
        @Index(name = "index2", value = "mGender desc", unique = false)
}

索引和索引之間用逗號隔開。

【例六】創建主鍵索引

洗洗睡吧,在@Entity中只支持創建常規索引和唯一索引,主鍵索引在後面會講到。

  • createInDb的用法
@Entity(
        nameInDb = "T_STUDENT",
        indexes = {
                @Index(name = "index1", value = "mId, mName asc", unique = true),
                @Index(name = "index2", value = "mGender desc", unique = false)
        },
        createInDb = true

)

createInDb的默認值是true,如果改爲false,將不會在這個數據庫中建表。如果不想使用該實體,又不想刪除這個文件,那麼可以將createInDb的值改成false,等下次使用的話再改成true。

  • schema的用法

schema怎麼定義?

在模塊的的build.gradle文件裏,添加以下配置

//greendao配置
greendao {
    //版本號,升級時可配置
    schemaVersion 1
    daoPackage 'com.xxx.ooo.greendaodemo.gendao'
    targetGenDir 'src/main/java'
}

以上配置就是schema的定義了。

還有一種定義方法需要導入依賴包:

implementation 'org.greenrobot:greendao-generator:3.2.0'

導入這個依賴之後就可以創建schema對象

Schema schema = new Schema(1, "com.xxx.ooo.greendaodemo.gendao");
String schemaName1 = schema.getName();

schema的name成員變量就是schema架構名稱,在目前的版本中只能獲取架構名稱,不可以設置架構名稱。schema架構名稱的默認值是“default”。

那麼,schema在@Entity中的用法是怎樣的呢?

@Entity(
        nameInDb = "T_STUDENT",
        indexes = {
                @Index(name = "index1", value = "mId, mName asc", unique = true),
                @Index(name = "index2", value = "mGender desc", unique = false)
        },
        createInDb = true,
        schema = "default"

)

當前版本只能將schema配置成"default",也就是說,當前版本只有一個架構,期待後期版本的更新會支持吧。

  • active的用法

設置Entity是否是活動的。如果是活動的,那麼他將有更新,刪除,刷新的方法。默認值爲false。

如果爲true,實體類中就會自動生成以下代碼

/**
 * Convenient call for {@link org.greenrobot.greendao.AbstractDao#delete(Object)}.
 * Entity must attached to an entity context.
 */
@Generated(hash = 128553479)
public void delete() {
    if (myDao == null) {
        throw new DaoException("Entity is detached from DAO context");
    }
    myDao.delete(this);
}
/**
 * Convenient call for {@link org.greenrobot.greendao.AbstractDao#refresh(Object)}.
 * Entity must attached to an entity context.
 */
@Generated(hash = 1942392019)
public void refresh() {
    if (myDao == null) {
        throw new DaoException("Entity is detached from DAO context");
    }
    myDao.refresh(this);
}
/**
 * Convenient call for {@link org.greenrobot.greendao.AbstractDao#update(Object)}.
 * Entity must attached to an entity context.
 */
@Generated(hash = 713229351)
public void update() {
    if (myDao == null) {
        throw new DaoException("Entity is detached from DAO context");
    }
    myDao.update(this);
}
/** called by internal mechanisms, do not call yourself. */
@Generated(hash = 1701634981)
public void __setDaoSession(DaoSession daoSession) {
    this.daoSession = daoSession;
    myDao = daoSession != null ? daoSession.getStudentDao() : null;
}
  • generateConstructors的用法

是否生成帶有全部參數的構造方法,默認值爲true。

@Generated(hash = 7034290)
public Student(Long mId, String mName, int mGender) {
    this.mId = mId;
    this.mName = mName;
    this.mGender = mGender;
}

如果設置爲false,那麼會生成不帶參數的構造方法

public Student() {
}
  • generateGettersSetters的用法

默認值爲true。是否生成getXXX和setXXX,這個比較簡單。

  • protobuf 的用法

GreenDao 支持 protocol buffer(protobuf) 協議數據的直接存儲,如果你通過 protobuf 協議與服務器交互,將不需要任何的映射。

可以參考一下我的另一篇博客

Android之協議緩衝區(Protocol Buffers)詳解

(8)基礎屬性註解
  • @Id

@Id修飾的成員變量爲主鍵,主鍵的基本數據類型必須是Long或者long,可以通過@Id(autoincrement = true)設置自增長,被@Id修飾的成員變量也被稱之爲主鍵索引

@Entity
public class Student {
    @Id(autoincrement = true)
    private Long mId;
    private String mName;
    private int mGender;
    
    ...
}
  • @Property

設置一個非默認關係映射所對應的列名,默認是的使用字段名。

比如:

@Property(nameInDb = "tName")
private String mName;

查看錶字段之後,發現mName字段變成了tName字段,如圖

  • @NotNul

設置數據庫表當前列不能爲空。

@Id(autoincrement = true)
@NotNull
private Long mId;

@Property(nameInDb = "tName")
private String mName;

@NotNull
private int mGender;
  • @Transient

添加次標記之後不會生成數據庫表的列。

  • @Deprecated

元素已被棄用,程序員可以使用它,但不鼓勵使用,通常是因爲它很危險,或者因爲存在更好的選擇。編譯器在不推薦使用的程序元素在不推薦使用的代碼中使用或重寫。

  • @Generated@Keep

@Generated: GreenDao運行所產生的構造函數或者方法,被此標註的代碼可以變更或者下次運行時清除。
@Keep: 註解的代碼段在GreenDao下次運行時保持不變。

  • @Convert

GreenDao默認支持的類型參數有

boolean,Boolean,int, Integer,short,Short,long,Long,float,Float,double,Double,byte,Byte,byte[],String,Date

如果greenDao的默認參數類型滿足不了你的需求,比如你想定義一個顏色屬性,那麼你可以使用數據庫支持的原生數據類型通過PropertyConverter類轉換成你想要的顏色屬性。

@Convert(converter = ColorConverter.class, columnType = Integer.class)
private Color mColor;

public enum Color {
    WHITE(0xffffff), BLACK(0x000000), YELLOW(0xfff000);
    final int id;
    Color(int id) {
        this.id = id;
    }
}

public static class ColorConverter implements PropertyConverter<Color, Integer> {
    //將Integer值轉換成Color值
    @Override
    public Color convertToEntityProperty(Integer databaseValue) {
        if (databaseValue == null) {
            return null;
        }
        for (Color color : Color.values()) {
            if (color.id == databaseValue) {
                return color;
            }
        }
        return Color.WHITE;
    }

    //將Color值轉換成Integer值
    @Override
    public Integer convertToDatabaseValue(Color entityProperty) {
        return entityProperty == null ? null : entityProperty.id;
    }
}
(9)索引註解

文章前面講到:

【一】在實體註解中可以配置索引集合,一個索引可以綁定一個或多個字段,並且只能創建常規索引和唯一索引。

【二】使用@Id修飾的字段爲主鍵,也可以稱之爲主鍵索引。

現在,我們來說明一個另外兩個有關索引的註解:@Index和@Unique

  • @Index

該註解有三個屬性,分別是name ,value,unique ,如下

@Index(name = "ageName", value = "age", unique = false)
private int age;

name爲索引名稱,如果沒有指定,greendao將會默認命名一個名稱。
value爲字段名稱,必須和字段名稱一模一樣,如果單獨修飾某字段,可以不指定,即

@Index(name = "ageName", unique = false)
private int age;

unique 爲true時,是唯一索引,爲false時,是常規索引。

  • @Unique

標誌位唯一索引

@Unique
private int age;
(10)關係註解

關係註解有兩種, 分別是@ToOne和@ToMany

  • @ToOne 一對一的關係

實體和實體之間一對一的關係。

我們來舉一個學生和學生檔案的關係,一個學生對應一個學生檔案,一個學生檔案對應一個學生。

創建學生實體:

@Entity
public class Student {

    @Id(autoincrement = true)
    @NotNull
    private Long mId;//表格唯一ID

    @Unique
    private String ID;//身份證

    private int mAge;//年齡

    private String hobby;//興趣愛好

    ...

}

創建學生檔案實體:

@Entity
public class StudentArchives {

    @Id(autoincrement = true)
    @NotNull
    private Long mId;

    @Unique
    private String ID;//身份證

    private String city;//城市

    //...

}

以上兩個實體有一個共同的屬性,那就是身份證,且是唯一的。那麼如何使兩個實體變成一對一的關係呢?

修改學生實體:

@Entity
public class Student {

    @Id(autoincrement = true)
    @NotNull
    private Long mId;//表格唯一ID

    @Unique
    private String identityID;//身份證

    private int mAge;//年齡

    private String hobby;//興趣愛好

    @ToOne(joinProperty = "identityID")//將身份證設置成外鍵,對應着學生檔案表中的主鍵
    StudentArchives studentArchives;

}

修改學生檔案實體:

@Entity
public class StudentArchives {

    @Id
    @NotNull
    private String identityID;//身份證

    private String city;//城市
    

}

使用@ToOne註解來標識兩個實體一對一的關係,其中joinProperty的作用是:將學生表中的identityID字段作爲外鍵約束,關聯學生檔案表中的主鍵。

這樣兩個實體就關聯成一對一的關係了。

  • @ToMany 一對多的關係

@ToMany定義兩個實體之間一對多的關係,如圖:

學生和班級是兩個實體,一個學生屬於一個班級,一個班級有多個學生。

下面分別創建兩個實體。

學生實體:

@Entity
public class Student {

    @Id(autoincrement = true)
    @NotNull
    private Long mId;//表格唯一ID

    @Unique
    private String identityID;//身份證

    private int mAge;//年齡

    private String hobby;//興趣愛好

    private Long classId;//班級ID
    
}

班級實體:

@Entity
public class StudentClass {

    @Id(autoincrement = true)
    @NotNull
    private Long mId;

    private String name;//班級名稱

    @ToMany(referencedJoinProperty = "classId")//classId爲學生表中的classId字段,和班級表中的mId對應
    @OrderBy(value = "mAge desc")
    List<Student> list;


}

使用@ToMany來表示實體間一隊多的關係,referencedJoinProperty指定學生表中的“classId”字段,“classId”字段與班級表中的主鍵對應。

@OrderBy(value = "mAge desc")指集合按照mAge字段倒序排序,如果需要正序排序,就改成:

@OrderBy(value = "mAge asc")

還有一種比較複雜的一對多的對應關係,即多個字段有一對多的關係,請往下看:

學生實體類:

@Entity
public class Student {

    @Id(autoincrement = true)
    @NotNull
    private Long mId;//表格唯一ID

    @Unique
    private String identityID;//身份證

    private int mAge;//年齡

    private String hobby;//興趣愛好

    private Long classId;//班級ID

    private String className;//班級名稱

}

班級實體類:

@Entity
public class StudentClass {

    @Id(autoincrement = true)
    @NotNull
    private Long mId;

    private String name;//班級名稱

    @ToMany(joinProperties = {
            @JoinProperty(name = "mId", referencedName = "classId"),
            @JoinProperty(name = "name", referencedName = "className")
    })
    @OrderBy(value = "mAge desc")
    List<Student> list;

}
  • @JoinEntity 多對多的關係

如果兩個實體是多對多的關係,那麼需要第三張表(表示兩個實體關係的表)

例如學生與課程是多對多關係,需要一個選課表來轉成兩個一對多的關係(學生與選課是一對多,課程與選課是一對多)

學生表:

@Entity
public class Student {

    @Id(autoincrement = true)
    @NotNull
    private Long mId;//表格唯一ID

    @Unique
    private String identityID;//身份證

    private int mAge;//年齡

    @ToMany
    @JoinEntity(
            entity = Student_Course.class,
            sourceProperty = "studentId",
            targetProperty = "courseId"
    )
    private List<Course> list;

}

課程表:

@Entity
public class Course {

    @Id(autoincrement = true)
    @NotNull
    private Long mId;//表格唯一ID

    private String course;//課程

}

學生和課程之間的關係表:

@Entity
public class Student_Course {

    @Id(autoincrement = true)
    @NotNull
    private Long mId;//表格唯一ID

    private Long studentId;//學生ID

    private Long courseId;//課程ID
}
(11)獲取數據庫

在Application中獲取或新建數據庫:

    DaoMaster.DevOpenHelper helper = new DaoMaster.DevOpenHelper(this, "demo.db", null);
    SQLiteDatabase db = helper.getWritableDatabase();
    DaoMaster daoMaster = new DaoMaster(db);
    daoSession = daoMaster.newSession();

public static DaoSession getDaoSession(){
    return daoSession;
}

最終需要隨時可以獲取DaoSession對象,進而操作數據庫。

(12)插入數據
  • 當主鍵類型是long(非Long)類型時,不支持自增長,實體如下:
@Entity
public class Student {

    @Id
    private long mId;//表格唯一ID

    @Unique
    private String identityID;//身份證

    @Index(unique = true)
    private int mAge;//年齡

    private String hobby;//興趣愛好

    private long classId;//班級ID

    private String className;//班級名稱

}

當設置字段值時,必須設置mId的值,並且設置的值唯一,這裏設置當前時間戳,比如:

            StudentDao studentDao = MyCustomApplication.getDaoSession().getStudentDao();
            for(int i=0;i<20;i++){
                Student student = new Student();
                student.setHobby("打球");
                student.setClassId(12 + i);
                student.setIdentityID("320911199006728299"+i);
                student.setClassName("強化班");
                student.setMAge(12);
                student.setMId(System.currentTimeMillis());
                studentDao.insert(student);
            }
  • 當主鍵類型是Long(非long)類型時,支持自增長,實體如下:
@Entity
public class Student {

    @Id
    private Long mId;//表格唯一ID

    @Unique
    private String identityID;//身份證

    @Index(unique = true)
    private int mAge;//年齡

    private String hobby;//興趣愛好

    private long classId;//班級ID

    private String className;//班級名稱

}

不管autoincrement爲true還是爲false時,主鍵都支持自增成長,也就是說autoincrement屬性無效(這是GreenDao的一個坑,希望在以後的版本會修復吧)
當然,如果設置了mId值,那麼將失去自增長特性,反之,如果沒有設置mId值,則可以實現自增長。自增長的mId的值從1開始遞增。不設置mId的代碼如下:

            for(int i=0;i<20;i++){
                Student student = new Student();
                student.setHobby("打球");
                student.setClassId(12 + i);
                student.setIdentityID("320911199006728299"+i);
                student.setClassName("強化班");
                student.setMAge(12);
                studentDao.insert(student);
            }

插入之後的表格如下:

  • 官方推薦的主鍵類型是Long或者long型,但是僅僅是推薦,並不代表一定需要按照推薦的來,實體如下:
@Entity
public class Student {

    @Id
    private String mId;//表格唯一ID

    @Unique
    private String identityID;//身份證

    private int mAge;//年齡

    private String hobby;//興趣愛好

    private long classId;//班級ID

    private String className;//班級名稱

}

由於主鍵是唯一的(不能重複),所以我們需要保證主鍵唯一,否則插入數據失敗,可以將UUID作爲唯一標識:

student.setMId(UUID.randomUUID().toString());
  • 當主鍵不是long或者Long類型時,將不支持自增長。

  • 使用@Unique註解的字段必須保證唯一,否則插入失敗。

  • 具有唯一索引@Index(unique = true)的字段也必須保證唯一,否則插入失敗。

  • 插入操作的方法介紹

 //這是最簡單的插入語句,新增一行數據,返回值爲行號
 public long insert(T entity)

 //傳遞一個數組,新增多行數據
 public void insertInTx(T... entities)

 //傳遞一個集合,新增多行數據
public void insertInTx(Iterable<T> entities)

//傳遞一個集合,新增多行數據,setPrimaryKey:是否設置主鍵
public void insertInTx(Iterable<T> entities, boolean setPrimaryKey)

//將給定的實體插入數據庫,若此實體類存在,則覆蓋
public long insertOrReplace(T entity)

//使用事務操作,將給定的實體插入數據庫,若此實體類存在,則覆蓋
public void insertOrReplaceInTx(T... entities)

//使用事務操作,將給定的實體插入數據庫,若此實體類存在,則覆蓋
public void insertOrReplaceInTx(Iterable<T> entities)

//使用事務操作,將給定的實體插入數據庫,若此實體類存在,則覆蓋,並設置是否設定主鍵
public void insertOrReplaceInTx(Iterable<T> entities, boolean setPrimaryKey)

//將給定的實體插入數據庫,但不設定主鍵
public long insertWithoutSettingPk(T entity)

insert和insertOrReplace的區別?

  1. insert只新增數據,如果主鍵字段的值重複或者唯一索引字段的值重複,那麼插入失敗。
  2. insertOrReplace新增數據或者替換數據,當主鍵重複時則直接替換數據,如果主鍵不同則插入數據。

總結: 如果保證主鍵唯一性?

  1. 當主鍵類型是Long時,不要給主鍵傳值,否則自增長特性失效。(autoincrement屬性無效)
@Id(autoincrement = true)
  1. 如果主鍵類型是long或者Long時,主鍵傳值可以是當前時間戳,可保證唯一。
student.setMId(System.currentTimeMillis());

3.如果主鍵類型是字符串時,主鍵傳值可以是UUID,可保證唯一。

student.setMId(UUID.randomUUID().toString());
(13)查詢

查詢分爲Dao查詢、

  • Dao查詢
//根據主鍵來查詢一條數據
public T load(K key)

//根據行號來查詢一條數據,行號從1開始
public T loadByRowId(long rowId) 

//查詢表中所有的數據
public List<T> loadAll()
  • QueryBuilder 查詢

返回值爲QueryBuilder的常見方法:

// 條件,AND 連接
public QueryBuilder<T> where(WhereCondition cond, WhereCondition... condMore)

// 條件,OR 連接
public QueryBuilder<T> whereOr(WhereCondition cond1, WhereCondition cond2, WhereCondition... condMore)

//去重
public QueryBuilder<T> distinct()

//分頁
public QueryBuilder<T> limit(int limit)

//偏移結果起始位,配合limit(int)使用
public QueryBuilder<T> offset(int offset)

//排序,升序
public QueryBuilder<T> orderAsc(Property... properties)

//排序,降序
public QueryBuilder<T> orderDesc(Property... properties)

// 排序,自定義
public QueryBuilder<T> orderCustom(Property property, String customOrderForProperty)

// 排序,SQL 語句
public QueryBuilder<T> orderRaw(String rawOrder)

//本地化字符串排序,用於加密數據庫無效
public QueryBuilder<T> preferLocalizedStringOrder()

//自定義字符串排序,默認不區分大小寫
public QueryBuilder<T> stringOrderCollation(String stringOrderCollation)

返回值爲WhereCondition的常見方法:

//嵌套條件且,用法同and
public WhereCondition and(WhereCondition cond1, WhereCondition cond2, WhereCondition... condMore)

//嵌套條件或者,用法同or
public WhereCondition or(WhereCondition cond1, WhereCondition cond2, WhereCondition... condMore)

返回結果的方法:

//值返回一條結果,如果返回結果的數量>1,則報錯。如果返回結果的數量等於0,那麼返回null。
public T unique()

//值返回一條結果,如果返回結果的數量>1或=0,則報錯。
public T uniqueOrThrow()

//返回一個集合
public List<T> list()

//讓我們通過按需加載數據(懶惰)來迭代結果。數據未緩存。必須關閉
public CloseableListIterator<T> listIterator()

//實體按需加載到內存中。首次訪問列表中的元素後,將加載並緩存該元素以供將來使用。必須關閉。
public LazyList<T> listLazy()

//實體的“虛擬”列表:對列表元素的任何訪問都會導致從數據庫加載其數據。必須關閉。
public LazyList<T> listLazyUncached()

有關排序的方法:

//正序排序
public QueryBuilder<T> orderAsc(Property... properties)

//倒序排序
public QueryBuilder<T> orderDesc(Property... properties)

有關多表查詢的方法:

//多表查詢
public <J> Join<T, J> join(Class<J> destinationEntityClass, Property destinationProperty)
public <J> Join<T, J> join(Property sourceProperty, Class<J> destinationEntityClass)
public <J> Join<T, J> join(Property sourceProperty, Class<J> destinationEntityClass, Property destinationProperty)
public <J> Join<T, J> join(Join<?, T> sourceJoin, Property sourceProperty, Class<J> destinationEntityClass, Property destinationProperty)

條件判斷:

//等於
eq()

//不等於
notEq() 

//值等於
like()

//取中間範圍
between()

//in命令
in()

//not in 命令
notIn()

//大於
gt()

//小於
lt()

//大於等於
ge()

//小於等於
le()

//爲空
isNull()

//不爲空
isNotNull()

【舉例一】 查詢所有數據

            List<Student> list = studentDao.queryBuilder()
                    .list();

【舉例二】 根據條件查詢

            List<Student> list = studentDao.queryBuilder()
                    .where(StudentDao.Properties.MAge.eq(12))
                    .where(StudentDao.Properties.MId.eq(1))
                    .list();

【舉例三】 查詢前10條數據,偏移量爲5

            List<Student> list = studentDao.queryBuilder()
                    .offset(5)
                    .limit(10)
                    .list();

從第6條數據開始查詢,只返回10條數據。

【舉例四】 查詢className中有“強”的所有數據(模糊查詢)

            List<Student> list = studentDao.queryBuilder()
                    .where(StudentDao.Properties.ClassName.like("%強%"))
                    .list();

這裏的%代表“強”前面和後面可能存在字符,如果改成

Properties.ClassName.like("強%")

表示,模糊查詢以“強”爲開頭的數據。

【舉例五】 返回按照年齡倒序排序之後的結果

            List<Student> list = studentDao.queryBuilder()
                    .orderDesc(StudentDao.Properties.MAge)
                    .list();

【舉例六】 in操作符演示

            List<Student> list = studentDao.queryBuilder()
                    .where(StudentDao.Properties.MId.in(10))
                    .list();

in的參數可以傳遞一個對象(如果傳遞基本數據類型,那麼自動轉成對象),也可以傳遞一個集合。

假設現在有一張表,如圖:

現在要求篩選出興趣愛好爲Hobby1和Hobby2的數據。

我們可以用whereOr和or操作符來實現

            List<Student> list = studentDao.queryBuilder()
                    .whereOr(StudentDao.Properties.Hobby.eq("Hobby1"), StudentDao.Properties.Hobby.eq("Hobby2"))
                    .list();

當然,使用in操作符也能篩選出興趣愛好爲Hobby1和Hobby2的數據,代碼如下:

            List<Student> list = studentDao.queryBuilder()
                    .where(StudentDao.Properties.Hobby.in("Hobby1", "Hobby2"))
                    .list();

其中文意思是,在Student表中篩選出Hobby字段中包含“Hobby1”和“Hobby2”的數據。

結果集合如下:

假設現在有兩張表,第一張是STUDENT表,第二張是STUDENT_CLASS表,如圖:

假設,查詢STUDENT表中“CLASS_NAME”字段值和STUDENT_CLASS表中的“NAME”字段值相同的數據。

代碼如下:

            StudentClassDao studentClassDao = MyCustomApplication.getDaoSession().getStudentClassDao();
            List<String> classNames = new ArrayList<>();

            for(StudentClass studentClass : studentClassDao.queryBuilder().list()){
                classNames.add(studentClass.getName());
            }
            StudentDao studentDao = MyCustomApplication.getDaoSession().getStudentDao();
            List<Student> list = studentDao.queryBuilder()
                    .where(StudentDao.Properties.ClassName.in(classNames))
                    .list();

結果集合如下:

【舉例七】 使用sql語句查詢

            List<Student> list = studentDao.queryBuilder()
                    .where(new WhereCondition.StringCondition("CLASS_NAME=?", "Class1"))
                    .list();

相當於sql語句

select * from STUDENT stu where stu.CLASS_NAME = "Class1"

還有一種寫法是:

List<Student> list = studentDao.queryRaw("where CLASS_NAME = ?", "Class1");

相當於sql語句

select * from STUDENT stu where stu.CLASS_NAME = "Class1"

【舉例八】 多表查詢(連接)

假設有兩張表,第一張是STUDENT表,第二張是STUDENT_CLASS表。

說到多表查詢,您可能會不由自主的想到表連接表連接分爲三大類:

  • 內連接

內連接又稱之爲普通連接或者自然連接。

sql語句如下:

//顯性
select * from STUDENT stu inner join STUDENT_CLASS stu_c on stu._id = stu_c._id where stu_c.NAME = "Class2"

//隱性
select * from STUDENT stu , STUDENT_CLASS stu_c on stu._id = stu_c._id where stu_c.NAME = "Class2"

結果集合如下:

  • 外連接

外連接又分爲左外連接、右外連接和全外連接,我們一般說成左連接右連接全連接

【左連接】(左表取全部,右表取部分)

select * from STUDENT stu left join STUDENT_CLASS stu_c on stu._id = stu_c._id

或者

select * from STUDENT stu left outer join STUDENT_CLASS stu_c on stu._id = stu_c._id

結果集合如下

【右連接】(左表取部分,右表取全部)和【全連接】(左右兩表都取全部)

這個瞭解即可,sqlite不支持右連接和全連接。

  • 交叉連接

交叉連接又被稱之爲笛卡爾積

sql語句:

select * from STUDENT stu cross join STUDENT_CLASS stu_c

笛卡爾積的結果相當於多表相乘的結果。STUDENT表有10條數據,STUDENT_CLASS有3條數據,那麼笛卡爾積的結果有10x3 = 30條數據。

以上三種連接就說到這裏,如果講的具體點篇幅比較長,不是本文的重點。之所以提到是因爲,接下來將講解GreenDao的多表查詢Join。

//多表查詢
public <J> Join<T, J> join(Class<J> destinationEntityClass, Property destinationProperty)
public <J> Join<T, J> join(Property sourceProperty, Class<J> destinationEntityClass)
public <J> Join<T, J> join(Property sourceProperty, Class<J> destinationEntityClass, Property destinationProperty)
public <J> Join<T, J> join(Join<?, T> sourceJoin, Property sourceProperty, Class<J> destinationEntityClass, Property destinationProperty)

GreenDao爲連接查詢(多表查詢)提供了四個構造方法,這四個構造方法其實是一樣的,所以我們不需要研究其中的區別。
GreenDao的Join接近於內連接,但又不像。內連接的結果集是包含兩表的所有字段,而GreenDao的Join只返回源表滿足條件的行。

參數說明:

sourceProperty:源表字段
destinationProperty:目標表字段
destinationEntityClass:目標表實體Class
sourceJoin:源連接(Join)

基本使用如圖:

如圖所示,從上到下總共有5個框:

第一個框確定了源表,如果你想要查詢的數據在A表,那麼就把A當做源表。
第二個框是源表的查詢條件,根據查詢條件生成一個新的結果集。
第四個框是目標表的查詢條件,根據查詢條件生成一個新的結果集。
第三個框確定了目標表,並將原表的結果集和目標表的結果集相連,相連之後就是一個Join,這個Join可以繼續和第三個表相連,形成一個新的Join。
第五個框就是連接(Join)之後的最終結果集,最終結果集爲源表的數據。

總結:GreenDao的連接查詢沒有內連接、外連接、交叉連接的概念,GreenDao的Join基本可以滿足一切多表查詢的需求。

【舉例九】 多表查詢(關係)

GreenDao不僅可以通過Join實現多表查詢,還可以通過關係實現多表查詢,關係分爲一對一一對多多對多,這三個概念前面已經說過了,這裏就用一對一關係來舉例。

一對一關係可以用@ToOne註解來標誌,比如以下兩個實體:

@Entity
public class Student {

    @Id
    private Long mId;//表格唯一ID

    @Unique
    private String identityID;//身份證

    private int mAge;//年齡

    private String hobby;//興趣愛好

    private long classId;//班級ID

    private String className;//班級名稱

    //...
}


@Entity
public class StudentClass {

    @Id
    private Long mId;

    private String name;//班級名稱

    private Long classId;//班級ID


    @ToOne(joinProperty = "mId")//將身份證設置成外鍵,對應着學生檔案表中的主鍵
    Student student;

    //....
}

兩個實體之間存在一對一的關係(類似於兩個實體存在依賴關係),思路是:

先獲取StudentClass對象,再獲取Student對象,代碼如下:

    StudentClassDao studentClassDao = MyCustomApplication.getDaoSession().getStudentClassDao();
    StudentClass studentClass = studentClassDao.loadDeep(1l);
    Student student = studentClass.getStudent();

因爲兩個實體存在一對一的關係,所以StudentClassDao自動生成了loadDeep和queryDeep這兩個方法。

(14)更新
public void update(T entity)
public void updateInTx(T... entities)
public void updateInTx(Iterable<T> entities)

以上三個方法很簡單,就是更新一條或者多條數據。

已有表格數據如下:

現在,如果想要將_id爲8的數據中的HOBBY修改爲“Hobby10”,代碼如下:

            StudentDao studentDao = MyCustomApplication.getDaoSession().getStudentDao();
            Student student = new Student();
            student.setMId(Long.valueOf(8));
            student.setHobby("Hobby10");
            studentDao.update(student);

我們新建一個Student對象,其中主鍵不能爲空,因爲總是先根據主鍵來找到對應的數據,再進行更新操作。修改的效果如下:

我們發現,除了_id和HOBBY之外,其它字段的值都變化了,所以我們需要改進一下代碼。

            StudentDao studentDao = MyCustomApplication.getDaoSession().getStudentDao();
            Student student = studentDao.load(Long.valueOf(8));
            student.setHobby("Hobby10");
            studentDao.update(student);

更新後的最終效果如下:

(15)插入或更新
public void save(T entity)
public void saveInTx(T... entities)
public void saveInTx(Iterable<T> entities)

我們先查看一下源碼

public void save(T entity) {
    if (hasKey(entity)) {
        update(entity);
    } else {
        insert(entity);
    }
}

傳入一個實體entity,如果entity的主鍵在表中已存在,則更新已有數據,反之,則插入新數據。

(16)刪除
  • dao刪除
//根據實體,刪除一條數據(默認根據主鍵刪除數據)
public void delete(T entity)

//刪除所有數據
public void deleteAll()

//根據key刪除數據
public void deleteByKey(K key)

//刪除多條數據
public void deleteInTx(T... entities)

//根據主鍵數組,刪除多條數據
public void deleteByKeyInTx(K... keys)

//根據集合,刪除多條數據
public void deleteByKeyInTx(Iterable<K> keys)
  • DeleteQuery

假如想要,刪除主鍵值爲10的數據

            studentDao.queryBuilder()
                    .where(StudentDao.Properties.MId.eq(Long.valueOf(10)))
                    .buildDelete()
                    .executeDeleteWithoutDetachingEntities();
(17)開啓日誌
QueryBuilder.LOG_SQL = true;
QueryBuilder.LOG_VALUES = true;
(18)事務

插入,刪除、更新數據庫都是高風險操作,如果任務執行了一半突然中斷是非常危險的事情。這個時候可以使用事務,如果任務突然中斷可以回滾到初始狀態。如圖所示,GreenDao事務的處理已經幫我們做好了。

(19)RxDao、RxQuery、RxTransaction

GreenDao支持RxJava,不懂RxJava的同學建議學習一下。但是當前最新GreenDao版本是“3.2.2”,最新版本只支持RxJava1,不支持RxJava2。

我不使用RxJava1,所以就不介紹了。我就滿懷期待的等待GreenDao對RxJava2的支持,說真的,很期待,因爲RxJava可以使代碼更新優雅,線程控制更加完美,代碼也看起來簡潔。

(20)升級

在項目開發中,數據庫升級顯得尤爲重要。

在模塊下,我們已經配置了schema

greendao {
    //版本號,升級時可配置
    schemaVersion 1
    daoPackage 'com.xxx.ooo.greendaodemo.gendao'
    targetGenDir 'src/main/java'
}

我們發現數據庫當前版本號爲1,如果改成2,就會執行升級代碼,GreenDao3.0已經幫我們自動生成了升級代碼,首先在DaoMaster找到以下代碼

/** WARNING: Drops all table on Upgrade! Use only during development. */
public static class DevOpenHelper extends OpenHelper {
    public DevOpenHelper(Context context, String name) {
        super(context, name);
    }

    public DevOpenHelper(Context context, String name, CursorFactory factory) {
        super(context, name, factory);
    }

    @Override
    public void onUpgrade(Database db, int oldVersion, int newVersion) {
        Log.i("greenDAO", "Upgrading schema from version " + oldVersion + " to " + newVersion + " by dropping all tables");
        dropAllTables(db, true);
        onCreate(db);
    }
}

當數據庫版本有變化時,就會執行onUpgrade回調方法,默認的升級邏輯是,先刪除所有的表格,再重新創建所有的表格

        dropAllTables(db, true);
        onCreate(db);

這個操作是愚蠢的,數據庫升級怎麼能允許刪除原有數據?

所以,我們必須重寫onUpgrade方法,具體代碼如下:

public class MyDevOpenHelper extends DaoMaster.DevOpenHelper {
    public MyDevOpenHelper(Context context, String name) {
        super(context, name);
    }

    public MyDevOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
        super(context, name, factory);
    }

    @Override
    public void onUpgrade(Database db, int oldVersion, int newVersion) {
        switch (oldVersion) {
            case 1:
                upgradeToVersion2(db);
            case 2:
                upgradeToVersion3(db);
            default:
                break;
        }
    }

    /**
     * 更新到版本2
     * @param db
     */
    private void upgradeToVersion2(Database db){
        Log.d("aaa", "更新到版本2");
    }

    /**
     * 更新到版本3
     * @param db
     */
    private void upgradeToVersion3(Database db){
        Log.d("aaa", "更新到版本3");
    }
}


    MyDevOpenHelper helper = new MyDevOpenHelper(this, "demo.db");
    SQLiteDatabase db = helper.getWritableDatabase();
    DaoMaster daoMaster = new DaoMaster(db);
    daoSession = daoMaster.newSession();

以上代碼已經可以實現數據庫的升級操作了,當數據庫新增字段時,可以在upgradeToVersionX方法裏面去新增一個字段,但是這樣做有兩個很明顯的缺點

  • 每次數據庫變化,都要編寫代碼做升級操作;
  • 萬一在某次升級代碼寫錯了,升級之後是一個高危風險;

那麼有沒有什麼辦法實現自動升級呢?

答案是有的。

接下來,我要粘貼一下網上現成的工具類

/**
 *
 * please call {@link #migrate(SQLiteDatabase, Class[])} or {@link #migrate(Database, Class[])}
 *
 */
public final class MigrationHelper {

    public static boolean DEBUG = false;
    private static String TAG = "MigrationHelper";
    private static final String SQLITE_MASTER = "sqlite_master";
    private static final String SQLITE_TEMP_MASTER = "sqlite_temp_master";

    private static WeakReference<ReCreateAllTableListener> weakListener;

    public interface ReCreateAllTableListener{
        void onCreateAllTables(Database db, boolean ifNotExists);
        void onDropAllTables(Database db, boolean ifExists);
    }

    public static void migrate(SQLiteDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        printLog("【The Old Database Version】" + db.getVersion());
        Database database = new StandardDatabase(db);
        migrate(database, daoClasses);
    }

    public static void migrate(SQLiteDatabase db, ReCreateAllTableListener listener, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        weakListener = new WeakReference<>(listener);
        migrate(db, daoClasses);
    }

    public static void migrate(Database database, ReCreateAllTableListener listener, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        weakListener = new WeakReference<>(listener);
        migrate(database, daoClasses);
    }

    public static void migrate(Database database, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        printLog("【Generate temp table】start");
        generateTempTables(database, daoClasses);
        printLog("【Generate temp table】complete");

        ReCreateAllTableListener listener = null;
        if (weakListener != null) {
            listener = weakListener.get();
        }

        if (listener != null) {
            listener.onDropAllTables(database, true);
            printLog("【Drop all table by listener】");
            listener.onCreateAllTables(database, false);
            printLog("【Create all table by listener】");
        } else {
            dropAllTables(database, true, daoClasses);
            createAllTables(database, false, daoClasses);
        }
        printLog("【Restore data】start");
        restoreData(database, daoClasses);
        printLog("【Restore data】complete");
    }

    private static void generateTempTables(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        for (int i = 0; i < daoClasses.length; i++) {
            String tempTableName = null;

            DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]);
            String tableName = daoConfig.tablename;
            if (!isTableExists(db, false, tableName)) {
                printLog("【New Table】" + tableName);
                continue;
            }
            try {
                tempTableName = daoConfig.tablename.concat("_TEMP");
                StringBuilder dropTableStringBuilder = new StringBuilder();
                dropTableStringBuilder.append("DROP TABLE IF EXISTS ").append(tempTableName).append(";");
                db.execSQL(dropTableStringBuilder.toString());

                StringBuilder insertTableStringBuilder = new StringBuilder();
                insertTableStringBuilder.append("CREATE TEMPORARY TABLE ").append(tempTableName);
                insertTableStringBuilder.append(" AS SELECT * FROM ").append(tableName).append(";");
                db.execSQL(insertTableStringBuilder.toString());
                printLog("【Table】" + tableName +"\n ---Columns-->"+getColumnsStr(daoConfig));
                printLog("【Generate temp table】" + tempTableName);
            } catch (SQLException e) {
                Log.e(TAG, "【Failed to generate temp table】" + tempTableName, e);
            }
        }
    }

    private static boolean isTableExists(Database db, boolean isTemp, String tableName) {
        if (db == null || TextUtils.isEmpty(tableName)) {
            return false;
        }
        String dbName = isTemp ? SQLITE_TEMP_MASTER : SQLITE_MASTER;
        String sql = "SELECT COUNT(*) FROM " + dbName + " WHERE type = ? AND name = ?";
        Cursor cursor=null;
        int count = 0;
        try {
            cursor = db.rawQuery(sql, new String[]{"table", tableName});
            if (cursor == null || !cursor.moveToFirst()) {
                return false;
            }
            count = cursor.getInt(0);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return count > 0;
    }


    private static String getColumnsStr(DaoConfig daoConfig) {
        if (daoConfig == null) {
            return "no columns";
        }
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < daoConfig.allColumns.length; i++) {
            builder.append(daoConfig.allColumns[i]);
            builder.append(",");
        }
        if (builder.length() > 0) {
            builder.deleteCharAt(builder.length() - 1);
        }
        return builder.toString();
    }


    private static void dropAllTables(Database db, boolean ifExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) {
        reflectMethod(db, "dropTable", ifExists, daoClasses);
        printLog("【Drop all table by reflect】");
    }

    private static void createAllTables(Database db, boolean ifNotExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) {
        reflectMethod(db, "createTable", ifNotExists, daoClasses);
        printLog("【Create all table by reflect】");
    }

    /**
     * dao class already define the sql exec method, so just invoke it
     */
    private static void reflectMethod(Database db, String methodName, boolean isExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) {
        if (daoClasses.length < 1) {
            return;
        }
        try {
            for (Class cls : daoClasses) {
                Method method = cls.getDeclaredMethod(methodName, Database.class, boolean.class);
                method.invoke(null, db, isExists);
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private static void restoreData(Database 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");

            if (!isTableExists(db, true, tempTableName)) {
                continue;
            }

            try {
                // get all columns from tempTable, take careful to use the columns list
                List<TableInfo> newTableInfos = TableInfo.getTableInfo(db, tableName);
                List<TableInfo> tempTableInfos = TableInfo.getTableInfo(db, tempTableName);
                ArrayList<String> selectColumns = new ArrayList<>(newTableInfos.size());
                ArrayList<String> intoColumns = new ArrayList<>(newTableInfos.size());
                for (TableInfo tableInfo : tempTableInfos) {
                    if (newTableInfos.contains(tableInfo)) {
                        String column = '`' + tableInfo.name + '`';
                        intoColumns.add(column);
                        selectColumns.add(column);
                    }
                }
                // NOT NULL columns list
                for (TableInfo tableInfo : newTableInfos) {
                    if (tableInfo.notnull && !tempTableInfos.contains(tableInfo)) {
                        String column = '`' + tableInfo.name + '`';
                        intoColumns.add(column);

                        String value;
                        if (tableInfo.dfltValue != null) {
                            value = "'" + tableInfo.dfltValue + "' AS ";
                        } else {
                            value = "'' AS ";
                        }
                        selectColumns.add(value + column);
                    }
                }

                if (intoColumns.size() != 0) {
                    StringBuilder insertTableStringBuilder = new StringBuilder();
                    insertTableStringBuilder.append("REPLACE INTO ").append(tableName).append(" (");
                    insertTableStringBuilder.append(TextUtils.join(",", intoColumns));
                    insertTableStringBuilder.append(") SELECT ");
                    insertTableStringBuilder.append(TextUtils.join(",", selectColumns));
                    insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";");
                    db.execSQL(insertTableStringBuilder.toString());
                    printLog("【Restore data】 to " + tableName);
                }
                StringBuilder dropTableStringBuilder = new StringBuilder();
                dropTableStringBuilder.append("DROP TABLE ").append(tempTableName);
                db.execSQL(dropTableStringBuilder.toString());
                printLog("【Drop temp table】" + tempTableName);
            } catch (SQLException e) {
                Log.e(TAG, "【Failed to restore data from temp table 】" + tempTableName, e);
            }
        }
    }

    private static List<String> getColumns(Database db, String tableName) {
        List<String> columns = null;
        Cursor cursor = null;
        try {
            cursor = db.rawQuery("SELECT * FROM " + tableName + " limit 0", null);
            if (null != cursor && cursor.getColumnCount() > 0) {
                columns = Arrays.asList(cursor.getColumnNames());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null)
                cursor.close();
            if (null == columns)
                columns = new ArrayList<>();
        }
        return columns;
    }

    private static void printLog(String info){
        if(DEBUG){
            Log.d(TAG, info);
        }
    }

    private static class TableInfo {
        int cid;
        String name;
        String type;
        boolean notnull;
        String dfltValue;
        boolean pk;

        @Override
        public boolean equals(Object o) {
            return this == o
                    || o != null
                    && getClass() == o.getClass()
                    && name.equals(((TableInfo) o).name);
        }

        @Override
        public String toString() {
            return "TableInfo{" +
                    "cid=" + cid +
                    ", name='" + name + '\'' +
                    ", type='" + type + '\'' +
                    ", notnull=" + notnull +
                    ", dfltValue='" + dfltValue + '\'' +
                    ", pk=" + pk +
                    '}';
        }

        private static List<TableInfo> getTableInfo(Database db, String tableName) {
            String sql = "PRAGMA table_info(" + tableName + ")";
            printLog(sql);
            Cursor cursor = db.rawQuery(sql, null);
            if (cursor == null)
                return new ArrayList<>();

            TableInfo tableInfo;
            List<TableInfo> tableInfos = new ArrayList<>();
            while (cursor.moveToNext()) {
                tableInfo = new TableInfo();
                tableInfo.cid = cursor.getInt(0);
                tableInfo.name = cursor.getString(1);
                tableInfo.type = cursor.getString(2);
                tableInfo.notnull = cursor.getInt(3) == 1;
                tableInfo.dfltValue = cursor.getString(4);
                tableInfo.pk = cursor.getInt(5) == 1;
                tableInfos.add(tableInfo);
                // printLog(tableName + ":" + tableInfo);
            }
            cursor.close();
            return tableInfos;
        }
    }
}

MigrationHelper的使用方法如下:

public class MyDevOpenHelper extends DaoMaster.DevOpenHelper {
    public MyDevOpenHelper(Context context, String name) {
        super(context, name);
    }

    public MyDevOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
        super(context, name, factory);
    }

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

        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);
            }
        }, StudentDao.class, StudentClassDao.class);

    }
}

以上代碼可以實現數據庫的自動升級(無論是刪除字段還是新增字段)

(21)其它
  • 當數據庫的數據有很多時,比如一百萬條數據

【一】 GreenDao是有內存的,如果插入上萬條數據,GreenDao本身的緩存設計導致內存佔用比較大,我們可以清空Session中的緩存

daoSession.clear();

或者清空Dao中的緩存

mBookDao.detachAll();

當然,清除緩存的存在可不僅僅是因爲佔用了內存,有時候在項目中,您會發現查詢到的數據竟然是內存中修改後的數據,而不是數據庫中的數據,這個時候可以清除緩存來解決。

【二】 當您從100萬條數據中查找某個條件(比如:age > 15),返回所有滿足條件的數據。由於數據太多,查詢所消耗的時間比較長,這個時候,您可以將age字段添加爲索引,以增加查詢速度。

【三】 當您插入或刪除數據時,建議先刪除索引,再進行插入或刪除操作,最後再添加索引。因爲一邊插入或刪除數據一邊維護索引所消耗的資源是恐怖的。

【四】 當查詢、插入、刪除、修改數據量比較龐大時,所花費的時間是比較多的,這時,您可以考慮在異步線程執行,防止卡UI線程,文章前面提到過的RxJava可以對線程進行控制。除了RxJava能控制線程之外,DaoSession也可以開啓線程,代碼如下:

            MyCustomApplication.getDaoSession().runInTx(new Runnable() {
                @Override
                public void run() {

                }
            });

只是這種方式沒有RxJava優雅罷了。

  • 多用戶的情況

可以採用多用戶多數據庫的設計方案(即每個用戶都有獨立的數據庫)。

[本章完...]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章