Android 使用 GreenDAO 3.x 进行增删改查和升级

定义

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'	//指定目标路径
	}
	……
}

添加测试模型

  1. 创建 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;
}
  1. 按下 Alt + Insert 快捷键,选择自动生成 Getter and SettertoString(用来打印查询结果)
  2. 编译项目(Make Project,Ctrl + F9),然后就会在配置的包名下生成对应的 DaoMaster、DaoSession、【实体名】Dao。
    (注意:一定要按顺序操作,先创建实体类,然后编译才会生成相应文件)
    Android 使用 GreenDAO 3.x 进行增删改查和升级_第1张图片

自定义类型

下面是一个枚举类型的自定义类,可以是任意类

/**
 * 记录模式
 */
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 {

    @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 类型。
Android 使用 GreenDAO 3.x 进行增删改查和升级_第2张图片

实体注解

@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  long insert(T entity) {
    @SuppressWarnings("unchecked")
    // Dao的第一个参数是"实体"类型,第二个参数是"主键"类型
    AbstractDao dao = (AbstractDao) getDao(entity.getClass());
    return dao.insert(entity);
}

public AbstractDao getDao(Class 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 entities) 批量插入数据库
  • insertInTx(T... entities) 批量插入数据库
  • insertInTx(Iterable entities, boolean setPrimaryKey) 批量插入数据库(主键用实体类中定义的,或数据库自动生成)
  • insertOrReplaceInTx(Iterable entities, boolean setPrimaryKey) 批量插入数据库(主键用实体类中定义的,或数据库自动生成)
  • insertOrReplaceInTx(Iterable entities) 批量插入数据库,如果已存在则替换该项
  • insertOrReplaceInTx(T... entities) 批量插入数据库,如果已存在则替换该项

  • delete(T entity) 删除单个数据
  • deleteInTx(Iterable entities) 批量删除数据
  • deleteInTx(T... entities) 批量删除数据
  • deleteByKey(K key) 通过主键删除数据
  • deleteByKeyInTx(Iterable entities) 通过主键批量删除数据
  • deleteByKeyInTx(K... keys) 通过主键批量删除数据
  • deleteAll() 删除所有数据

  • update(T entity) 根据主键修改实体
  • updateInTx(Iterable entities) 批量修改实体
  • updateInTx(T... entities) 批量修改实体

修改这里有的特殊,通过源码可以看的 where 语句里用的是 pkColumns,pk指的是主键(一般是Long类型的 Id),因此如果直接用上述的方法去修改,那么之前就必须先查询一次,把待修改的字段提出来,然后再根据 Id 再去数据库修改该条字段。
Android 使用 GreenDAO 3.x 进行增删改查和升级_第3张图片

  • T load(K key) 通过主键查询实体
  • List loadAll() 查询所有元素
  • T loadByRowId(long rowId) 通过行号获取元素
  • QueryBuilder queryBuilder() 创建查询构造器
  • List queryRaw(String where, String... selectionArg) 使用 sql 的 where 语句进行查询
  • Query queryRawCreate(String where, Object... selectionArg) 使用 sql 的 where 语句创建查询对象,可用于拼接
  • Query queryRawCreateListArgs(String where, Collection selectionArg) 使用 sql 的 where 语句创建查询对象,可用于拼接
  • void refresh(T entity) 根据主键,从数据库中重新加载实体
  • 执行数据库语句需要和创建数据库时的线程保持一致,否则会报错,因此 Query 就有个 forCurrentThread,方法可以让查询在原线程调用。

    public Query forCurrentThread() {
       return queryData.forCurrentThread(this);
    }
    

    使用sql语句

    有时候需求比较复杂,GreenDAO 提供的 api 实现不了你的需求,这时候就需要用到 sql 语句了(该情况主要增对:增、删、改;至于查询,GreenDAO 原生的方法就够用了)

    daoSession.getDatabase().execSQL(sql)

    简单的增删改,可以借助 SqlUtils.java 这个 GreenDAO 的工具类来实现,就不用重新造轮子了。
    例如如果除 Id 外,还有其它固定不变的唯一标识,就可以在不查询的前提下,直接修改,这样就省了一次操作数据库的步骤
    Android 使用 GreenDAO 3.x 进行增删改查和升级_第4张图片

    更新数据库

    GreenDAO 自带的 OpenHelper 只是一个用于开发版本的数据库辅助工具,它在发现数据库版本升级时,会删除所有的数据库,然后重新创建。
    Android 使用 GreenDAO 3.x 进行增删改查和升级_第5张图片

    原生方式

    所以在实际生产环境,需要继承并重写 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,并添加如下内容
    Android 使用 GreenDAO 3.x 进行增删改查和升级_第6张图片

    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>... 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。

    解决方法如下

    1. 在 module 的 build.gradle 文件中加上
    dependencies {
    	implementation 'androidx.multidex:multidex:2.0.1'
    }
    

    因为我的项目已经是基于 androidx 的了所以引用的都是 androidx 的包,如果你的项目还是基于 android support,请使用 'com.android.support:multidex:1.0.3'

    1. 在 manifest.xml 中声明自定义的 Application
    
    
    
    ...
    
    
    
    1. 自定义的 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'
    }
    

    你可能感兴趣的:(#,Android,Database)