greenDao:操作数据库的开源框架
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包的获取:https://files.cnblogs.com/files/tgyf/greendao-3.1.1.rar,下载后不用解压,直接将后缀改为“.jar”即可。
e. 注意,还需要用同样的方法下载jar包freemarker和greendao-generator,同样在上面的下载页面搜索就好:
https://files.cnblogs.com/files/tgyf/freemarker-1.19.2.rar,
https://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框架的思想及用法之后,处理一般性的数据集时会简单、高效很多,通过面向对象的思想为实体类生成对应的操作类,结构清晰,易维护。
最后给出项目下载链接:https://files.cnblogs.com/files/tgyf/greendaoTest.rar。