greenDAO: Android ORM for your SQLite database
1. greenDao庫獲取
英文標題借鑒的是greendrobot官網介紹greenDao時給出的Title,鏈接:http://greenrobot.org/greendao/,有興趣的可以點進去(不用外網),里面有使用說明及相關資料的下載接口。greenDao目前版本已經更新到3.x.x,與2.x.x相比支持的功能發生了很大的改變(后面會提到),所以強烈推薦使用最新的版本。如3.1.1版本的獲取方法如下:
a. 通過鏈接進入官網,點擊右邊的greenDao按鈕進入其GitHub頁面:
b. greenDao GitHub頁面上的“Add greenDAO to your project”模塊是針對Android Studio開發者的引用庫添加說明,在app和project build.gradle文件中分別編寫以下代碼並進行同步(Studio界面上有一個同步按鈕,不會的自己查)就可以使用greenDao提供的方法來操作SQLite數據庫了:
1 buildscript { 2 repositories { 3 mavenCentral() 4 } 5 dependencies { 6 classpath 'org.greenrobot:greendao-gradle-plugin:3.1.0' 7 } 8 } 9 //上面是在project的build.gradle文件中,下面是在app的build.gradle文件中,具體見后面實例 10 apply plugin: 'org.greenrobot.greendao' 11 12 dependencies { 13 compile 'org.greenrobot:greendao:3.1.0' 14 }
當然,人家也說了“Please ensure that you are using the latest versions by checking here and here”,即點進去瞧瞧版本更新到多少了,條件允許的話盡量用最新的。目前這兩個鏈接分別對應版本3.1.1和3.1.0,如果在Studio中想用3.1,1,就將上述代碼中的3.1.0改為3.1.1。
c. 以版本3.1.1為例,點擊checking here后會進入其資源下載頁面(包括Eclipse開發者喜愛的jar包文件):
如果使用的集成開發環境是Eclipse,那么會與Studio不同,在導入庫方面還是較原始(需要自行下載jar包並添加到項目中,有些jar包難找的時候體會最深),點擊圖中的jar按鈕即可開始下載。
d. 想偷懶的可以直接點擊鏈接進行jar包的獲取:http://files.cnblogs.com/files/tgyf/greendao-3.1.1.rar,下載后不用解壓,直接將后綴改為“.jar”即可。
e. 注意,還需要用同樣的方法下載jar包freemarker和greendao-generator,同樣在上面的下載頁面搜索就好:
http://files.cnblogs.com/files/tgyf/freemarker-1.19.2.rar,
http://files.cnblogs.com/files/tgyf/greendao-generator-3.1.0.rar,下載后處理方式如上面紅色字體描述——改后綴名"rar"->"jar"。
這三個jar包的作用分別是:
greendao——SQLite數據庫操作核心,對一些常用的方法進行了封裝;
generator——根據需求表對應實體類生成相關的greendao類,如上面提到的DaoSession等;
freemarker——將自動生成的類以文本形式輸出;
至於在開發中用哪個版本合適,視實際情況而定。能用Google親生的Studio最好不過了,不用下載jar包,還有人家對eclipse已經不進行新特性的支持了。
2. greenDao操作SQLite數據庫
前面提到,新版本與老版本在支持的功能上有了很大的提升,最明顯的地方就是gen目錄下文件的生成時機或者說是方法。有老版本使用經驗的小伙伴應該清楚,要想在android中利用greenDao相關類來處理SQLite數據庫,必須先在另一個Java工程中建立需求表的實體類,生成gen目錄下的文件(如xxxDao、DaoSession以及DaoMaster等),然后將gen目錄拷入android工程中方能使用。當然,也可以將gen目錄的生成路徑直接定位到android工程中,免得每次修改實體類並重新編譯后都需要拷貝這些文件。
不過,這些問題在新版中就不存在了,因為實體類的定義與gen目錄的生成都可以直接在android工程中完成。下面就來看看Studio中具體是怎么回事吧,至於對舊版本的用法感興趣的自己去研究咯。
2.1 在Studio中新建一個project greendaoTest,過程中根據需求設置各種屬性,默認布局為顯示一個字串“Hello World!”。其實如果只是對greenDao框架進行學習與測試,可以利用數據庫查詢工具或log打印內容來檢測操作結果,不是必須將內容顯示在布局組件中。
2.2 在項目中添加庫依賴代碼,完整文件代碼分別如下:
project build.gradle:
1 // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 3 buildscript { 4 repositories { 5 jcenter() 6 } 7 dependencies { 8 classpath 'com.android.tools.build:gradle:2.1.0' 9 classpath 'org.greenrobot:greendao-gradle-plugin:3.1.1' //greendao 10 11 // NOTE: Do not place your application dependencies here; they belong 12 // in the individual module build.gradle files 13 } 14 } 15 16 allprojects { 17 repositories { 18 jcenter() 19 } 20 } 21 22 task clean(type: Delete) { 23 delete rootProject.buildDir 24 }
app build.gradle文件:
1 apply plugin: 'com.android.application' 2 apply plugin: 'org.greenrobot.greendao' //greendao 3 4 android { 5 compileSdkVersion 24 6 buildToolsVersion "24.0.0" 7 8 greendao{ //greendao 9 schemaVersion 1 10 targetGenDir 'src/main/java' 11 } 12 defaultConfig { 13 applicationId "com.learn.greendaotest" 14 minSdkVersion 22 15 targetSdkVersion 24 16 versionCode 1 17 versionName "1.0" 18 } 19 buildTypes { 20 release { 21 minifyEnabled false 22 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 } 24 } 25 } 26 27 dependencies { 28 compile fileTree(dir: 'libs', include: ['*.jar']) 29 testCompile 'junit:junit:4.12' 30 compile 'com.android.support:appcompat-v7:24.0.0' 31 compile 'org.greenrobot:greendao:3.1.1' //greendao 32 }
和greenDao相關的語句,末尾都加了注釋“greenDao”,方便區分與維護。可以看出,這里導入的是最新的版本3.1.1。
2.3 在與主類文件MainActivity.java同一目錄下定義表——實體類Student,代碼如下:
1 package com.learn.greendaotest; 2 3 import org.greenrobot.greendao.annotation.Entity; 4 import org.greenrobot.greendao.annotation.Id; 5 import org.greenrobot.greendao.annotation.Property; 6 7 @Entity 8 public class Student { 9 @Id 10 private Long id; 11 @Property(nameInDb = "NAME") 12 private String name; 13 @Property(nameInDb = "AGE") 14 private int age; 15 }
簡單起見,聲明了三個屬性:id、name、age,分別表示學生的序號、名字、年齡,其中序號是唯一的。當然,如果需要,可以設置序號的順序、屬性的非空等限制條件。定義好實體類之后,只需要運行程序(或者點擊build菜單中的make project/make module 'app'選項)就可以生成相關的greenDao操作類了。
首先,仍舊看Student這個類,發現多了以下代碼:
1 @Generated(hash = 352757281) 2 public Student(Long id, String name, int age) { 3 this.id = id; 4 this.name = name; 5 this.age = age; 6 } 7 @Generated(hash = 1556870573) 8 public Student() { 9 } 10 public Long getId() { 11 return this.id; 12 } 13 public void setId(Long id) { 14 this.id = id; 15 } 16 public String getName() { 17 return this.name; 18 } 19 public void setName(String name) { 20 this.name = name; 21 } 22 public int getAge() { 23 return this.age; 24 } 25 public void setAge(int age) { 26 this.age = age; 27 }
代碼並不陌生,帶參數與不帶參數的構造函數,以及三個屬性的set/get方法,但是這些是自動生成的(包括接下來要講的三個類,會貼出部分關鍵代碼,所有的大家可以下載文末提供的源碼)。
表操作類StudentDao,可以說是greenDao生成的和Student類最親近的一個類了,關鍵的地方有以下幾個:
a. 內部類——屬性類Properties,關於屬性Property,greenDao還提供了一系列好用的方法,后面會提到。
1 public static class Properties { 2 public final static Property Id = new Property(0, Long.class, "id", true, "_id"); 3 public final static Property Name = new Property(1, String.class, "name", false, "NAME"); 4 public final static Property Age = new Property(2, int.class, "age", false, "AGE"); 5 }
b. 表創建與刪除函數——createTable(Database db, boolean ifNotExists)和dropTable(Database db, boolean ifExists),操作時是否做表存在判斷可以通過傳入參數“ifExists”來決定。由於調用對象就是表對應的類本身,所以指定表所屬數據庫對象就好(不用關心表名是什么)。
1 /** Creates the underlying database table. */ 2 public static void createTable(Database db, boolean ifNotExists) { 3 String constraint = ifNotExists? "IF NOT EXISTS ": ""; 4 db.execSQL("CREATE TABLE " + constraint + "\"STUDENT\" (" + // 5 "\"_id\" INTEGER PRIMARY KEY ," + // 0: id 6 "\"NAME\" TEXT," + // 1: name 7 "\"AGE\" INTEGER NOT NULL );"); // 2: age 8 } 9 10 /** Drops the underlying database table. */ 11 public static void dropTable(Database db, boolean ifExists) { 12 String sql = "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + "\"STUDENT\""; 13 db.execSQL(sql); 14 }
可以看出,自動生成的表創建代碼將屬性id、name、age類型設置為了數據庫中的類型INTEGER、TEXT、INTEGER,id唯一PRIMARY KEY,age NOT NULL,而大寫的名字是我們在定義Student類時自己聲明的,如name:@Property(nameInDb = "NAME")。
c. 實體對象獲取方法,提供了兩種方式:返回臨時引用和直接給引用參數賦值,而后者對屬性操作調用的是Student類中的set(Object object)方法。
1 @Override 2 public Student readEntity(Cursor cursor, int offset) { 3 Student entity = new Student( // 4 cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), // id 5 cursor.isNull(offset + 1) ? null : cursor.getString(offset + 1), // name 6 cursor.getInt(offset + 2) // age 7 ); 8 return entity; 9 } 10 11 @Override 12 public void readEntity(Cursor cursor, Student entity, int offset) { 13 entity.setId(cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0)); 14 entity.setName(cursor.isNull(offset + 1) ? null : cursor.getString(offset + 1)); 15 entity.setAge(cursor.getInt(offset + 2)); 16 }
類DaoSession,簡單點說就是用來獲取類StudentDao實例的,留意其構造函數和實例返回方法即可。
1 public DaoSession(Database db, IdentityScopeType type, Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> 2 daoConfigMap) { 3 super(db); 4 5 studentDaoConfig = daoConfigMap.get(StudentDao.class).clone(); 6 studentDaoConfig.initIdentityScope(type); 7 8 studentDao = new StudentDao(studentDaoConfig, this); 9 10 registerDao(Student.class, studentDao); 11 } 12 13 public StudentDao getStudentDao() { 14 return studentDao; 15 }
類DaoMaster,是和org.greenrobot.greendao.database.DatabaseOpenHelper最親近的類,那么到這里自定義的實體類Student和greenDao封裝的數據庫處理類終於聯系上了。該類中又定義了兩個靜態內部類OpenHelper和DevOpenHelper,前者重載了類DatabaseOpenHelper的表建立方法onCreate(Database db),后者重載了表更新方法onUpgrade(Database db, int oldVersion, int newVersion)。
1 /** 2 * Calls {@link #createAllTables(Database, boolean)} in {@link #onCreate(Database)} - 3 */ 4 public static abstract class OpenHelper extends DatabaseOpenHelper { 5 public OpenHelper(Context context, String name) { 6 super(context, name, SCHEMA_VERSION); 7 } 8 9 public OpenHelper(Context context, String name, CursorFactory factory) { 10 super(context, name, factory, SCHEMA_VERSION); 11 } 12 13 @Override 14 public void onCreate(Database db) { 15 Log.i("greenDAO", "Creating tables for schema version " + SCHEMA_VERSION); 16 createAllTables(db, false); 17 } 18 } 19 20 /** WARNING: Drops all table on Upgrade! Use only during development. */ 21 public static class DevOpenHelper extends OpenHelper { 22 public DevOpenHelper(Context context, String name) { 23 super(context, name); 24 } 25 26 public DevOpenHelper(Context context, String name, CursorFactory factory) { 27 super(context, name, factory); 28 } 29 30 @Override 31 public void onUpgrade(Database db, int oldVersion, int newVersion) { 32 Log.i("greenDAO", "Upgrading schema from version " + oldVersion + " to " + newVersion + " by dropping all tables"); 33 dropAllTables(db, true); 34 onCreate(db); 35 } 36 }
還定義了刪除所有表的方法dropAllTables(Database db, boolean ifExists),由於我們之前只定義了一個實體類(表),所以這里只有StudentDao類刪除表的語句。
/** Drops underlying database table using DAOs. */ public static void dropAllTables(Database db, boolean ifExists) { StudentDao.dropTable(db, ifExists); }
創建表也是如此:
1 /** Creates underlying database table using DAOs. */ 2 public static void createAllTables(Database db, boolean ifNotExists) { 3 StudentDao.createTable(db, ifNotExists); 4 }
以及和DaoSession類獲取StudentSao實例相似的newDevSession(Context context, String name)方法,用以獲取DaoSession類實例。
1 public static DaoSession newDevSession(Context context, String name) { 2 Database db = new DevOpenHelper(context, name).getWritableDb(); 3 DaoMaster daoMaster = new DaoMaster(db); 4 return daoMaster.newSession(); 5 }
而方法newSession()繼而會調用DaoSession類的三參構造函數:
1 public DaoSession newSession() { 2 return new DaoSession(db, IdentityScopeType.Session, daoConfigMap); 3 }
所以,不用擔心獲取DaoSession實例時該怎樣傳入參數,因為DaoMaster及其父類都已經完成了。如第三個參數daoConfigMap,在DaoMaster類中找不到,其父類AbstractDaoMaster完成了聲明與初始化工作。
1 protected final Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap; 2 3 public AbstractDaoMaster(Database db, int schemaVersion) { 4 this.db = db; 5 this.schemaVersion = schemaVersion; 6 7 daoConfigMap = new HashMap<Class<? extends AbstractDao<?, ?>>, DaoConfig>(); 8 }
2.4 准備工作差不多了,可以開始操作數據庫了。由於只是進行簡單的測試,所以不涉及界面,直接通過log打印的形式來描述結果了。
2.4.1 建立數據庫及實體類對應的表,第二個參數指定了數據庫的名稱,第三個參數(類型CursorFactory)一般為null即可。
1 DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(getApplicationContext(), "student.db", null);
結合之前給出的DaoMaster類代碼,這句代碼其實做了很多事情。執行DevOpenHelper類構造函數DevOpenHelper(Context context, String name, CursorFactory factory)時會調用OpenHelper類構造函數OpenHelper(Context context, String name, CursorFactory factory),進而又會調用基類DatabaseOpenHelper構造函數DatabaseOpenHelper(Context context, String name, CursorFactory factory, int version)與被重載方法onCreate(Database db),最后調用DaoMaster類的createAllTables(Database db, boolean ifNotExists)建立所有表。
數據庫與表格文件都建立好了,下面一步到位獲取StudentDao類實例:
1 DaoMaster daoMaster = new DaoMaster(devOpenHelper.getWritableDb()); 2 DaoSession daoSession = daoMaster.newSession(); 3 StudentDao studentDao = daoSession.getStudentDao();
2.4.2 插入兩條數據,一般情況下第一個參數id是不需要特殊指定的(null即可),會自動從1開始增序填入表中。
1 Student studentLl = new Student(null, "Lilei", 21); 2 Student studentHmm = new Student(null, "Hanmeimei", 21); 3 studentDao.insert(studentLl); 4 studentDao.insert(studentHmm);
關於insert(T entity)方法,在AbstractDao類中的定義如下:
1 /** 2 * Insert an entity into the table associated with a concrete DAO. 3 * 4 * @return row ID of newly inserted entity 5 */ 6 public long insert(T entity) { 7 return executeInsert(entity, statements.getInsertStatement(), true); 8 }
再往下追蹤能看得到不同層次的源碼,這里不打算全部列一遍。關注一下該方法的最后一句注釋:返回新插入實體在數據表中的行號,所以在對表中數據進行查詢之前,可以通過打印返回值來判斷插入是否成功。修改代碼,添加返回值獲取與打印:
long resultLl = studentDao.insert(studentLl); long resultHmm = studentDao.insert(studentHmm); showLog("row id of insert Lilei: "+resultLl); showLog("row id of insert Hanmeimei: "+resultHmm);
打印出的結果符合預期,表明插入成功了,因為之前已經執行過一次(row id為1和2),所以這里往后遞增為3和4。還可以看出id是從1開始的,而不像熟悉的數組下標是從0開始。
08-29 03:12:29.624 25326-25326/com.learn.greendaotest I/STUDENT: row id of insert Lilei: 3
08-29 03:12:29.624 25326-25326/com.learn.greendaotest I/STUDENT: row id of insert Hanmeimei: 4
showLog是什么鬼?自定義的一個用來打印log的方法,免得每次都需要傳入TAG標記以及必須傳入String類型參數值,適當的時候利用封裝偷偷懶。
1 private final String TAG = "STUDENT"; 2 private void showLog(Object message) { 3 Log.i(TAG, ""+message); 4 }
同樣地,Toast提示語可以封裝成:
private void showToast(Object message) { Toast.makeText(MainActivity.this, ""+message, Toast.LENGTH_SHORT).show(); }
2.4.3 數據查詢,並將結果打印出來,看看是否真的插入了四個實體值。
1 List<Student> listQuery = studentDao.queryBuilder().where(StudentDao.Properties.Id.between(1, 8)).limit(5).build().list(); 2 for (Student student : listQuery) { 3 showLog("id: "+student.getId()); 4 showLog("name: "+student.getName()); 5 showLog("age: "+student.getAge()); 6 }
08-29 03:36:28.418 13742-13742/com.learn.greendaotest I/STUDENT: id: 1
08-29 03:36:28.418 13742-13742/com.learn.greendaotest I/STUDENT: name: Lilei
08-29 03:36:28.419 13742-13742/com.learn.greendaotest I/STUDENT: age: 21
08-29 03:36:28.419 13742-13742/com.learn.greendaotest I/STUDENT: id: 2
08-29 03:36:28.419 13742-13742/com.learn.greendaotest I/STUDENT: name: Hanmeimei
08-29 03:36:28.419 13742-13742/com.learn.greendaotest I/STUDENT: age: 21
08-29 03:36:28.419 13742-13742/com.learn.greendaotest I/STUDENT: id: 3
08-29 03:36:28.419 13742-13742/com.learn.greendaotest I/STUDENT: name: Lilei
08-29 03:36:28.419 13742-13742/com.learn.greendaotest I/STUDENT: age: 21
08-29 03:36:28.419 13742-13742/com.learn.greendaotest I/STUDENT: id: 4
08-29 03:36:28.419 13742-13742/com.learn.greendaotest I/STUDENT: name: Hanmeimei
08-29 03:36:28.419 13742-13742/com.learn.greendaotest I/STUDENT: age: 21
解釋一下where(WhereCondition condition)和limit(int limit)這兩個方法,其實看代碼也能明白大概了。
where():添加數據查詢的篩選條件,除了這里的ID范圍,還可以是字串包含或不包含某個字符/字串等;
limit():只將查詢結果中的前五條賦給List對象,其余的忽略;
前面提到過,屬性類Property有很多好用的方法。比如這里的between(Object value1, Object value2)限定值的區間,查看該類的定義還可以發現eq(Object value)方法是用來比較值是否相等,等等返回值為WhereCondition實例的方法,說它是為了where()方法而生也不為過。
2.4.4 刪除數據,過程和查詢類似,只不過將符合條件的實體值刪除而已。
1 List<Student> listDelete = (List<Student>) studentDao.queryBuilder().where(StudentDao.Properties.Id.le(3)).build().list(); 2 for (Student student : listDelete) { 3 studentDao.delete(student); 4 }
以上代碼目的為刪除id小於等於3的行,那么再次輸出結果只剩下一條數據了:
08-29 03:59:10.631 2257-2257/com.learn.greendaotest I/STUDENT: id: 4
08-29 03:59:10.631 2257-2257/com.learn.greendaotest I/STUDENT: name: Hanmeimei
08-29 03:59:10.631 2257-2257/com.learn.greendaotest I/STUDENT: age: 21
2.4.5 更新數據,目前Student表中還剩下一條數據,那就將它的name字段給為Lilei吧。
1 Student stu = studentDao.queryBuilder() 2 .where(StudentDao.Properties.Id.ge(4), StudentDao.Properties.Name.like("%mei%")).build().unique(); 3 if (stu == null) { 4 showToast("實體不存在!"); 5 }else{ 6 stu.setName("Lilei"); 7 studentDao.update(stu); 8 }
注意,這里調用where()方法時傳入了兩個條件參數,其實還可以傳更多。參數的意義分別為id大於等於4,name字段中間部分的值為“mei”。最后的unique()方法使結果唯一,即查詢到一條馬上停止查詢過程並返回結果Student類實例,而不是List<Student>。如果沒有找到則彈出Toast字串提醒用戶,否則進行名字的修改。現在的查詢結果就變成下面這樣了:
08-29 04:08:48.788 10993-10993/? I/STUDENT: id: 4
08-29 04:08:48.788 10993-10993/? I/STUDENT: name: Lilei
08-29 04:08:48.788 10993-10993/? I/STUDENT: age: 21
2.4.6 升級數據庫,涉及到的重載方法onUpgrade(Database db, int oldVersion, int newVersion)已經在類DaoMaster中實現了,接下來需要做的是改變數據庫版本與實體類屬性。
在app build.gradle文件將數據庫版本值由1改為2:
1 greendao{ //greendao 2 schemaVersion 2 3 targetGenDir 'src/main/java' 4 }
在實體類Student中添加性別屬性sex(String型),
1 @Id 2 private Long id; 3 @Property(nameInDb = "NAME") 4 private String name; 5 @Property(nameInDb = "AGE") 6 private int age; 7 @Property 8 private String sex;
接着執行程序即可。可以發現:性別屬性沒有指定其在StudentDao->Properties類中的別名(在數據表中的列名),而自動生成的屬性別名為“SEX”。可知若沒有顯式聲明,那么就按全大寫來處理,否則按照指定的賦值,但是id除外(別名為_id)。
3. 總結
本文中的測試案例都是很簡單的情況,實際開發時需要處理的數據肯定要多得多。熟悉了greenDao框架的思想及用法之后,處理一般性的數據集時會簡單、高效很多,通過面向對象的思想為實體類生成對應的操作類,結構清晰,易維護。
最后給出項目下載鏈接:http://files.cnblogs.com/files/tgyf/greendaoTest.rar。