定义
greenDAO 官网:http://greenrobot.org/greendao/
greenDAO 的 Github 地址:https://github.com/greenrobot/greenDAO
greenDAO 是一款开源的,针对 Android 操作 SQLite 的 ORM 框架。它将 Java 对象映射到 SQLite 数据库中,使我们在操作数据库的时候不用编写 SQL 语句即可操纵数据库。
优点
- 轻量级
- 支持缓存
- 支持数据库加密
- 通过对象映射,操作简便
- 采用预处理,自动生成代码,而非反射,速度快效率高
添加依赖
首先在项目的 build.gradle 文件的 dependencies 中添加
buildscript {
repositories {
google()
jcenter()
mavenCentral() // add repository
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.1'
classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2'
……
}
}
然后在 module 的 build.gradle 中添加
apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao' // apply plugin
……
dependencies {
implementation 'org.greenrobot:greendao:3.2.2'
}
R8 或 ProGuard 混淆配置
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
public static java.lang.String TABLENAME;
}
-keep class **$Properties {*;}
# If you do not use SQLCipher:
-dontwarn net.sqlcipher.database.**
# If you do not use RxJava:
-dontwarn rx.**
配置版本
在 module 的 gradle 中添加
android {
……
greendao {
schemaVersion 1 //数据库版本号,迁移的时候用
daoPackage ‘com.example.test.greendao.gen’ //指定DaoMaster、DaoSession、【实体名】Dao 的包名
targetGenDir 'src/main/java' //指定目标路径
}
……
}
添加测试模型
- 创建 Note.java
@Entity(indexes = { @Index(value = "text, date DESC", unique = true)})
public class Note {
@Id(autoincrement = true) //该字段为主键,自增
private Long id;
@NotNull //字段不允许为空
private String text;
private Date date;
@Convert(converter = RecordConverter.class, columnType = Integer.class) //自定义转换器
private RecordMode mode;
@Transient //该字段不保存到数据库
private float noUse;
}
- 按下
Alt + Insert
快捷键,选择自动生成Getter and Setter
和toString
(用来打印查询结果) - 编译项目(Make Project,
Ctrl + F9
),然后就会在配置的包名下生成对应的 DaoMaster、DaoSession、【实体名】Dao。
(注意:一定要按顺序操作,先创建实体类,然后编译才会生成相应文件)
自定义类型
下面是一个枚举类型的自定义类,可以是任意类
/**
* 记录模式
*/
public enum RecordMode {
/** 正在编写 */
Draft(0),
/** 已保存 */
Saved(1),
/** 已上传 */
Uploaded(2);
private int value;
RecordMode(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static RecordMode newInstance(int value) {
RecordMode[] modes = values();
for(RecordMode mode : modes) {
if(mode.value == value) {
return mode;
}
}
return RecordMode.Draft;
}
}
然后构造一个转化器进行转化
public class RecordConverter implements PropertyConverter<RecordMode, Integer> {
@Override
public RecordMode convertToEntityProperty(Integer databaseValue) {
return RecordMode.newInstance(databaseValue);
}
@Override
public Integer convertToDatabaseValue(RecordMode entityProperty) {
return entityProperty.getValue();
}
}
最后在实体的属性上添加上 @Convert 注解即可
@Convert(converter = RecordConverter.class, columnType = Integer.class)
private RecordMode mode;
对于 Date 类型,虽然它不是 SQLite 支持的几种基本类型,但是它是Java中的类型,GreenDAO对它自动做了转换,将它转为 Integer 类型。
实体注解
@Entity:表明这个实体类会在数据库中生成一个与之相对应的表。
- nameInDb:表名默认为实体名,通过该变量可以自定义表名
- indexes:定义索引,多个列作为索引可以在字符串中用逗号分隔。unique 标识索引是否唯一。
- createInDb:如果有多个实体关联同一张表,可以在其它实体中将的该变量设为false,来避免表的重复创建(默认为 true)
- schema:当一个项目有多个数据库时,要标明这个dao属于哪个schema(schema即封装后的数据库)
- active:是否应该生成更新/删除/刷新方法。如果 Entity 定义了 @ToOne 或 @ToMany 关系,那么该值有效,指是否支持实体类之间的 update,refresh,delete 等操作
属性注解
- @Id:对应数据库表中的主键,是每条数据的唯一标识,可以通过设置 autoincrement 来使其自增。如果实体中没有声明主键,会默认创建一个Long类型的自增的主键 “_id”。
GreenDAO在每次启动应用时,会初始化ID为1,如果没加 autoincrement = true,第二次启动再 insert 就会报错。 - @Property:可以通过设置nameInDb,来指定该字段在数据库中的
- @NotNull:值不能为空
- @Transient:短暂的,标识该属性不会被存入数据库中
- @Unique:表明该属性的值在数据库中必须唯一。
- @Index:创建一个索引,通过name设置索引的别名,也可以通过unique给所有添加约束
- @Convert:指定一个PropertyConverter,用于支持自定义类型和sqlite支持的类型之间的转化
关系注解
- @ToOne:定义自己与一个实体对象的关系
- @ToMany:定义自己与多个实体对象的关系。
- @JoinProperty:对于更复杂的关系,可以使用这个注解标明目标属性的源属性
- @JoinEntity:多对多关系,有其它的表或实体时,可以给目标属性添加这个额外的注解
- @OrderBy:默认按主键ASC升序排列
派生注解
- @Generated:这个是编译之后 GreenDAO 自动生成的,注解内包含一个哈希值,来标识该方法的唯一性。它会自动生成实体类的 “无参构造函数” 和 “全参构造函数”。如果该函数已存在,则不会添加注解,否则自动生成并添加注解。
数据库操作
初始化
初始化 GreenDao 就是 初始化一个 DaoSession,该操作通常只会在整个 App 的生命周期中被调用一次。因此,我们通常把它放在 Application 里面或者使用惰性加载或单例(即当第一次使用数据库的时候才打开)
注意:“不加密数据库” 和 “加密数据库” 不能使用同一个名字,因为加密是对整个数据库加密,名字相同则路径相同是同一个文件,但两者一个不需要解密,一个要解密,会报文件不是数据库的错误。所以要分开定义。
private DaoSession daoSession;
private DaoSession daoEncryptSession;
private void initGreenDAO(Context context, String password) {
//未加密的数据库
DaoMaster.DevOpenHelper helper = new DaoMaster.DevOpenHelper(context, "RawDb");
Database db = helper.getWritableDb();
daoSession = new DaoMaster(db).newSession();
//加密的数据库
DaoMaster.DevOpenHelper encryptHelper = new DaoMaster.DevOpenHelper(context, "EncryptDb");
Database encryptDb = encryptHelper.getEncryptedWritableDb(password);
daoEncryptSession = new DaoMaster(encryptDb).newSession();
}
public DaoSession getDaoSession() {
return daoSession;
}
public DaoSession getDaoEncryptSession() {
return daoEncryptSession;
}
调用方式
后续的操作可以使用 DaoSession 直接调用,也可以通过 DaoSession 拿到相应的 Dao 再去调用,两者是一样的。
拿 insert 举个例子,在 AbstractDaoSession.java 的源码中是这样调用的
public <T> long insert(T entity) {
@SuppressWarnings("unchecked")
// Dao的第一个参数是"实体"类型,第二个参数是"主键"类型
AbstractDao<T, ?> dao = (AbstractDao<T, ?>) getDao(entity.getClass());
return dao.insert(entity);
}
public AbstractDao<?, ?> getDao(Class<? extends Object> entityClass) {
// entityToDao 是一个 map,通过实体类映射Dao元素,如果找到则返回
AbstractDao<?, ?> dao = entityToDao.get(entityClass);
if (dao == null) {
throw new DaoException("No DAO registered for " + entityClass);
}
return dao;
}
所以说,操作 DaoSession 来执行增删改查,实际上就是操作对应的 Dao,只是 GreenDAO 帮我们封装了一层而已。
增
insert(T entity)
将对象插入数据库insertInTx(Iterable<T> entities)
批量插入数据库insertInTx(T... entities)
批量插入数据库insertInTx(Iterable<T> entities, boolean setPrimaryKey)
批量插入数据库(主键用实体类中定义的,或数据库自动生成)insertOrReplaceInTx(Iterable<T> entities, boolean setPrimaryKey)
批量插入数据库(主键用实体类中定义的,或数据库自动生成)insertOrReplaceInTx(Iterable<T> entities)
批量插入数据库,如果已存在则替换该项insertOrReplaceInTx(T... entities)
批量插入数据库,如果已存在则替换该项
删
delete(T entity)
删除单个数据deleteInTx(Iterable<T> entities)
批量删除数据deleteInTx(T... entities)
批量删除数据deleteByKey(K key)
通过主键删除数据deleteByKeyInTx(Iterable<T> entities)
通过主键批量删除数据deleteByKeyInTx(K... keys)
通过主键批量删除数据deleteAll()
删除所有数据
改
update(T entity)
根据主键修改实体updateInTx(Iterable<T> entities)
批量修改实体updateInTx(T... entities)
批量修改实体
修改这里有的特殊,通过源码可以看的 where 语句里用的是 pkColumns,pk指的是主键(一般是Long类型的 Id),因此如果直接用上述的方法去修改,那么之前就必须先查询一次,把待修改的字段提出来,然后再根据 Id 再去数据库修改该条字段。
查
T load(K key)
通过主键查询实体List<T> loadAll()
查询所有元素T loadByRowId(long rowId)
通过行号获取元素QueryBuilder<T> queryBuilder()
创建查询构造器List<T> queryRaw(String where, String... selectionArg)
使用 sql 的 where 语句进行查询Query<T> queryRawCreate(String where, Object... selectionArg)
使用 sql 的 where 语句创建查询对象,可用于拼接Query<T> queryRawCreateListArgs(String where, Collection<Object> selectionArg)
使用 sql 的 where 语句创建查询对象,可用于拼接void refresh(T entity)
根据主键,从数据库中重新加载实体
执行数据库语句需要和创建数据库时的线程保持一致,否则会报错,因此 Query 就有个 forCurrentThread,方法可以让查询在原线程调用。
public Query<T> forCurrentThread() {
return queryData.forCurrentThread(this);
}
使用sql语句
有时候需求比较复杂,GreenDAO 提供的 api 实现不了你的需求,这时候就需要用到 sql 语句了(该情况主要增对:增、删、改;至于查询,GreenDAO 原生的方法就够用了)
daoSession.getDatabase().execSQL(sql)
简单的增删改,可以借助 SqlUtils.java 这个 GreenDAO 的工具类来实现,就不用重新造轮子了。
例如如果除 Id 外,还有其它固定不变的唯一标识,就可以在不查询的前提下,直接修改,这样就省了一次操作数据库的步骤
更新数据库
GreenDAO 自带的 OpenHelper 只是一个用于开发版本的数据库辅助工具,它在发现数据库版本升级时,会删除所有的数据库,然后重新创建。
原生方式
所以在实际生产环境,需要继承并重写 DaoMaster.OpenHelper 类的 onUpgrade 方法。
@Override
public void onUpgrade(Database db, int oldVersion, int newVersion) {
for (int j = oldVersion + 1; j <= newVersion; j++) {
switch (j) {
case 2:
break;
case 3:
break;
default:
break;
}
}
}
升级辅助库 GreenDaoUpgradeHelper
也可以使用升级辅助库 GreenDaoUpgradeHelper 类。
原理是:通过 MigrationHelper 在删表重建的过程中,使用临时表保存数据并还原,表格重建之后自动拼接字段名并填入值,然后批量重新插入新建的表中。
MigrationHelper 的 Github 地址 https://github.com/yuweiguocn/GreenDaoUpgradeHelper
添加依赖
在项目根路径的 build.gradle 中添加
allprojects {
repositories {
google()
jcenter()
// 用来引入 rxpermission, GreenDaoUpgradeHelper 等第三方库
maven { url 'https://jitpack.io' }
}
}
在 module 目录的 build.gradle 中添加
dependencies {
……
implementation 'org.greenrobot:greendao:3.2.2'
implementation 'io.github.yuweiguocn:GreenDaoUpgradeHelper:v2.2.1'
}
混淆规则
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
public static void dropTable(org.greenrobot.greendao.database.Database, boolean);
public static void createTable(org.greenrobot.greendao.database.Database, boolean);
}
代码实现
创建 MySQLiteOpenHelper.java,并添加如下内容
public class MySQLiteOpenHelper extends DaoMaster.OpenHelper {
public MySQLiteOpenHelper(Context context, String name) {
super(context, name);
}
public MySQLiteOpenHelper(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);
}
}, NoteDao.class);
}
}
migrate 函数的最后是实体类对应的 dao 的类型,有多少个实体类,这里就要加多少个 dao
migrate 函数原型如下
public static void migrate(Database database, ReCreateAllTableListener listener, Class<? extends AbstractDao<?, ?>>... daoClasses) {
weakListener = new WeakReference<>(listener);
migrate(database, daoClasses);
}
最后修改原先创建数据库时使用的 helper 即可
private void initGreenDAO(String password) {
//未加密的数据库
// DaoMaster.DevOpenHelper helper = new DaoMaster.DevOpenHelper(this, "RawDb");
MySQLiteOpenHelper helper = new MySQLiteOpenHelper(this, "RawDb");
Database db = helper.getWritableDb();
daoSession = new DaoMaster(db).newSession();
//加密的数据库
// DaoMaster.DevOpenHelper encryptHelper = new DaoMaster.DevOpenHelper(this, "EncryptDb");
MySQLiteOpenHelper encryptHelper = new MySQLiteOpenHelper(this, "EncryptDb");
Database encryptDb = encryptHelper.getEncryptedWritableDb(password);
daoEncryptSession = new DaoMaster(encryptDb).newSession();
}
常用的 sql 语句
- 增加
inert into table1 ( field1, field2 ) values ( value1, value2 )
- 删除
delete from table1 where 【筛选条件】
- 修改
update table1 set field1=value1, field2=value2 where 【筛选条件】
- 查询
select * from table1 where 【筛选条件】
- 模糊查询
select * from table1 where field1 like '%value1%'
- 排序(ASC升序 / DESC降序)
select * from table1 order by field1, field2 【ASC/DESC】
- 分页查询
select count as total from table1
- 求和
select sum(field1) as sumField1 from table1
- 求平均数
select avg(field1) as avgField1 from table1
- 求最大值
select max(field1) as maxField1 from table1
- 求最小值
select min(field1) as minField1 from table1
常见问题
调用 Database db = openHelper.getEncryptedWritableDb(password);
时报错,找不到对应的类,而获取普通未加密的数据库则一切正常。
(测试机是 vivo X20A,Android 8.1.0)
java.lang.NoClassDefFoundError: org.greenrobot.greendao.database.DatabaseOpenHelper$EncryptedHelper
at org.greenrobot.greendao.database.DatabaseOpenHelper.checkEncryptedHelper(DatabaseOpenHelper.java:121)
at org.greenrobot.greendao.database.DatabaseOpenHelper.getEncryptedWritableDb(DatabaseOpenHelper.java:133)
因为在 Android5.0 之前,每一个 android 应用中只会含有一个 dex 文件,但是这个 dex 的方法数量被限制在65535之内,这就是著名的 64K(64*1024) 事件。为了解决这个问题,Google 官方推出了这个类似于补丁一样的 support-library, MultiDex。
解决方法如下
- 在 module 的 build.gradle 文件中加上
dependencies {
implementation 'androidx.multidex:multidex:2.0.1'
}
因为我的项目已经是基于 androidx 的了所以引用的都是 androidx 的包,如果你的项目还是基于 android support,请使用 'com.android.support:multidex:1.0.3'
- 在 manifest.xml 中声明自定义的 Application
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.test">
<application
...
android:name="com.example.global.MyApplicatoin">
...
</application>
</manifest>
- 自定义的 Application 继承自 MultiDexApplication,重写 Application的attachBaseContext()这个方法。
public class MyApplication extends MultiDexApplication {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}
如果是 Android 5.0 以上的,那就是没加加密数据的模块了,在 module 的 build.gradle 上加上即可
dependencies {
//数据库加密
implementation 'net.zetetic:android-database-sqlcipher:4.2.0'
}