GreenDAO是一个对象关系映射(ORM)的框架,它能够提供一个接口通过操作对象的方式去操作关系型数据库,能够使操作数据库时更简单、更方便。

GreenDao相比与其他的ORM型数据库,具有的优点有:

1.依赖体积小(100KB左右)

2.最小的内存开销

3.易于使用的API

4.支持数据库加密

结合自己的了解,将这个框架整合了一下:支持增删改查,数据库存储路径设置,数据库升级以及遇到的坑等,基本可以满足95%以上的项目需求。

 

GreenDao原理

GreenDao中,DaoMaster.DevOpenHelper源码如下:

/** 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);
    }
}

点击super,如下:

greendao根据id更新数据 greendao数据库_GreenDao

可以发现:其实GreenDao也是基于原生SQLiteOpenHelper进行了封装,最后还是调用了原生的API。继承SqliteOpenHelper的原生数据库,我们会编写大量的Dao文件、SQL语句以及需要关注cursor的关闭时机以及事物管理等等,但是这些事情GreenDao帮我们很好的处理了,我们只需要关注逻辑的实现即可,从而加快了我们的开发效率。

GreenDao的使用:

1.1 配置

项目的build.gradle:

classpath 'org.greenrobot:greendao-gradle-plugin:3.2.1'

app的build.gradle:

apply plugin: 'org.greenrobot.greendao'
greendao {
    // 指定数据库schema版本号,迁移等操作会用到
    schemaVersion 1
    // 通过gradle插件生成的数据库相关文件的包名,默认为你的entity所在的包名
    daoPackage 'com.john.johngreendao.greendao.dao'
    // 生成的数据库文件默认目录为:build/generated/source/greendao
    // 自定义生成数据库文件的目录,可以将生成的文件放到java目录中,而不是build中,这样就不用额外的设置资源目录了
    targetGenDir 'src/main/java'
}

dependencies:

implementation 'org.greenrobot:greendao:3.2.0'

1.2 新建Entity实体类:

@Entity
public class Customer {
    @Id(autoincrement = true)
    private Long id;
    private String name;
    private int age;
    //新加字段
    private String sex;
    //新加字段
    private String like;

    public String getLike() {
        return like;
    }
    public void setLike(String like) {
        this.like = like;
    }
    public Long getId() {
        return this.id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return this.age;
    }
    public void setAge(int age) {
        this.age = age;
    }

    public String getSex() {
        return sex;
    }
    public void setSex(String sex) {
        this.sex = sex;
    }
}

关于greenDao的一些注解以及意义如下,可以参考一下:

@Entity:它会把当前类映射成默认以类名为表名的数据库表,每一个Entity对象对应着数据库表中的一行记录。当然像表名这些关于数据库表的设置也可以在 @Entity中进行设置。

@nameInDb 在数据库中的名字,如不写则为实体中类名
 @indexes 索引
 @createInDb 是否创建表,默认为true,false时不创建
 @schema 指定架构名称为实体
 @active 无论是更新生成都刷新

@Id 每条数据对应的位置,必写项

@Property:通过这个注解可以设置列名,默认是将属性名的写法转换成数据库中全大写的写法,如果不设置property属性列名就是NAME,如果有多个单词的话,每个单词中间会有下划线分隔开,前提是属性名是标准的驼峰命名法,这样才能识别每个单词。
@NotNull:限制这个列的插入不能为空

@Unique 唯一约束   该属性值必须在数据库中是唯一值
@ToMany 一对多
@OrderBy 排序
@ToOne 一对一 关系表
@generated 由greendao产生的构造函数或方法
@Transient:这个注解的意思就是只是一个临时数据,不会被写入到数据库中,所以上面的Company类中不会出现tempUsageCount属性的getter和setter方法了。

当然还有像标识索引的 @Index、表示数据库表之间关系的 @ToOne等一些注解,有需要可以去官网了解。

生成实体类后,Make Project ,会在自己设定的目录中生成DaoMaster和DaoSession以及Dao文件,如下:

greendao根据id更新数据 greendao数据库_Android_02

1.3 使用GreenDao:

项目Demo的目录如下:

 

greendao根据id更新数据 greendao数据库_GreenDao_03

1.3.1 初始化GreenDao

在MyApplication中使用GreenDaoManager类的init()方法:

public class GreenDaoManager {

    private static SQLiteDatabase db;
    private static DaoSession mDaoSession;

    /**
     * 初始化greenDao
     */
    public static void initDatabase(Context context) {
        // 通过 DaoMaster 的内部类 DevOpenHelper,你可以得到一个便利的 SQLiteOpenHelper 对象。
        // 可能你已经注意到了,你并不需要去编写「CREATE TABLE」这样的 SQL 语句,因为 greenDAO 已经帮你做了。
        // 注意:默认的 DaoMaster.DevOpenHelper 会在数据库升级时,删除所有的表,意味着这将导致数据的丢失。
        // 所以,在正式的项目中,你还应该做一层封装,来实现数据库的安全升级。
        //最基本DaoMaster.DevOpenHelper使用 开发阶段时用
        DaoMaster.DevOpenHelper mHelper = new DaoMaster.DevOpenHelper(context, "test-db", null);
        //自定义数据库版本控制 上线的时候用 将上面注销掉
        //        GreenDaoOpenHelper mHelper = new GreenDaoOpenHelper(new GreenDaoContextHelper(context), "test-db", null);
        db = mHelper.getWritableDatabase();
        // 注意:该数据库连接属于 DaoMaster,所以多个 Session 指的是相同的数据库连接。
        DaoMaster mDaoMaster = new DaoMaster(db);
        mDaoSession = mDaoMaster.newSession();
    }

    public static DaoSession getDaoSession() {
        return mDaoSession;
    }

    public static SQLiteDatabase getDb() {
        return db;
    }

}

1.3.2 操作greenDao

在GreenDaoMannager中使用 getDaoSession可以使用GreenDao的API,使用getDB可以使用原生数据库的API,相比比较灵活。

在MainActivity中,如下:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.add)
    Button add;
    @BindView(R.id.delete)
    Button delete;
    @BindView(R.id.update)
    Button update;
    @BindView(R.id.query)
    Button query;
    @BindView(R.id.upload)
    Button upload;
    @BindView(R.id.listView)
    ListView listView;

    private MyAdapter adapter;
    private UserDao userDao;
    private int age;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        initData();

    }

    private void initData() {
        userDao = GreenDaoManager.getDaoSession().getUserDao();
        for (int i = 0; i <= 4; i++) {//默认先添加5条数据 下次再进入APP后又会增加5条 只是为了演示 可以注释掉
            User user1 = new User();
            user1.setName("小力");
            user1.setAge(18);
            userDao.insert(user1);
        }
        List<User> users = userDao.loadAll();
        adapter = new MyAdapter(this, users);
        listView.setAdapter(adapter);
    }


    @OnClick({R.id.add, R.id.delete, R.id.update, R.id.query, R.id.upload})
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.add://增
                // id自增 全部name为小力 age为25
                User user1 = new User();
                user1.setName(age % 2 == 0 ? "小力" : "大力");
                user1.setAge(age);
                userDao.insert(user1);
                queryUserData();//刷新数据
                age++;
                break;
            case R.id.delete://删
                //删除姓名为小力的所有user
                Query<User> queryUsers = userDao.queryBuilder().where(UserDao.Properties.Name.eq("小力")).build();
                if (queryUsers == null) {
                    return;
                }
                for (User user : queryUsers.list()) {
                    userDao.delete(user);
                }
                queryUserData();//刷新数据
                break;
            case R.id.update://改
                // 将ID为5的user改为name 大力力 age改为11  注意是否有ID=5 没有的话就没有效果了
                //                User queryUser = userDao.queryBuilder().where(UserDao.Properties.Id.eq(5)).build().unique();
                //                queryUser.setName("大大大力力");
                //                queryUser.setAge(11);
                //                userDao.update(queryUser);
                //将name为 小力 的User 改为大力力 age改为 11
                Query<User> queryUser = userDao.queryBuilder().where(UserDao.Properties.Name.eq("大力")).build();
                for (User user : queryUser.list()) {
                    user.setName("大大力力");
                    user.setAge(11);
                    userDao.update(user);
                }
                queryUserData();//刷新数据
                break;
            case R.id.query://查
                queryUserData();//刷新数据
                break;
            case R.id.upload://更新
                //------上线后数据库的更新步骤------
                //1.将GreenDaoManager中DaoMaster.DevOpenHelper注释掉,将GreenDaoOpenHelper注释打开;
                //2.修改表结构后让表都具有set和get方法;
                //3.如果有新增加的表,将表的类名加到GreenDaoOpenHelper的onUpgrade方法中的MigrationHelper.migrate方法中
                //4.将app下build.gradle目录中greendao的schemaVersion版本号+1;
                //5.make工程。
                break;
        }
    }

    /**
     * 查询整张表
     */
    public void queryUserData() {
        List<User> users = userDao.loadAll();//查询整张表
        adapter.refreshData(users);//刷新数据
    }

}

里面涉及了数据库的增、删、改、查以及数据库更新的操作,看官们可以边下载DEMO看日志的输出,还是比较明显的,其中在增加数据的方法中我们需要注意的是,使用插入insert()方法,适用于插入较小的数据,如果数据量较大,使用insertInTx()方法开启事物插入数据效率会更高。更多关于增删改查的语句API如下:

void    attachEntity(T entity):

long    count():获取数据库中数据的数量

// 数据删除相关  
void    delete(T entity):从数据库中删除给定的实体
void    deleteAll() :删除数据库中全部数据
void    deleteByKey(K key):从数据库中删除给定Key所对应的实体
void    deleteByKeyInTx(java.lang.Iterable<K> keys):使用事务操作删除数据库中给定的所有key所对应的实体
void    deleteByKeyInTx(K... keys):使用事务操作删除数据库中给定的所有key所对应的实体
void    deleteInTx(java.lang.Iterable<T> entities):使用事务操作删除数据库中给定实体集合中的实体
void    deleteInTx(T... entities):使用事务操作删除数据库中给定的实体

// 数据插入相关  
long    insert(T entity):将给定的实体插入数据库
void    insertInTx(java.lang.Iterable<T> entities):使用事务操作,将给定的实体集合插入数据库
void    insertInTx(java.lang.Iterable<T> entities, boolean setPrimaryKey):使用事务操作,将给定的实体集合插入数据库,
并设置是否设定主键
void    insertInTx(T... entities):将给定的实体插入数据库
long    insertOrReplace(T entity):将给定的实体插入数据库,若此实体类存在,则覆盖
void    insertOrReplaceInTx(java.lang.Iterable<T> entities):使用事务操作,将给定的实体插入数据库,若此实体类存在,则覆盖
void    insertOrReplaceInTx(java.lang.Iterable<T> entities, boolean setPrimaryKey):使用事务操作,将给定的实体插入数据库,若此实体类存在,则覆盖
        并设置是否设定主键
void    insertOrReplaceInTx(T... entities):使用事务操作,将给定的实体插入数据库,若此实体类存在,则覆盖
long    insertWithoutSettingPk(T entity):将给定的实体插入数据库,但不设定主键

// 新增数据插入相关API  
void    save(T entity):将给定的实体插入数据库,若此实体类存在,则更新
void    saveInTx(java.lang.Iterable<T> entities):将给定的实体插入数据库,若此实体类存在,则更新
void    saveInTx(T... entities):使用事务操作,将给定的实体插入数据库,若此实体类存在,则更新

// 加载相关  
T   load(K key):加载给定主键的实体
java.util.List<T>     loadAll():加载数据库中所有的实体
protected java.util.List<T>   loadAllAndCloseCursor(android.database.Cursor cursor) :从cursor中读取、返回实体的列表,并关闭该cursor
protected java.util.List<T>   loadAllFromCursor(android.database.Cursor cursor):从cursor中读取、返回实体的列表
T   loadByRowId(long rowId) :加载某一行并返回该行的实体
protected T     loadUnique(android.database.Cursor cursor) :从cursor中读取、返回唯一实体
protected T     loadUniqueAndCloseCursor(android.database.Cursor cursor) :从cursor中读取、返回唯一实体,并关闭该cursor

//更新数据  
void    update(T entity) :更新给定的实体
protected void  updateInsideSynchronized(T entity, DatabaseStatement stmt, boolean lock)
protected void  updateInsideSynchronized(T entity, android.database.sqlite.SQLiteStatement stmt, boolean lock)
void    updateInTx(java.lang.Iterable<T> entities) :使用事务操作,更新给定的实体
void    updateInTx(T... entities):使用事务操作,更新给定的实体

1.4 数据库版本控制

由于我们的表都是以Java实体类的形式体现的,虽然我们在onUpgrade()方法下使用用sql语句是没有问题的,但是,我们在哪个实体类哪个版本做了更改如果没有注释的话是很难发现的。在开发阶段,我们使用的DaoMaster.OpenHelper的onUpgrade()方法上层是这样执行的:

/** 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);
    }
}

点进去dropAllTables(db,true)方法,如下:

/** Drops underlying database table using DAOs. */
public static void dropAllTables(Database db, boolean ifExists) {
    CustomerDao.dropTable(db, ifExists);
    StudentDao.dropTable(db, ifExists);
    UserDao.dropTable(db, ifExists);
}

里面有我创建的三张表的Dao文件,点CustomerDao的dropTable,如下:

/** Drops the underlying database table. */
public static void dropTable(Database db, boolean ifExists) {
    String sql = "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + "\"CUSTOMER\"";
    db.execSQL(sql);
}

其实那张表被删除了,所以,我们在实际上线过程中,版本一升级,之前的数据全部删除了,我们需要自定义一个类继承DaoMaster.OpenHelper类,重写onUpgrade(),将super()方法注释掉,重写我们自己的逻辑。在这里,介绍一个数据迁移的工具类,其原理是将之前版本的数据先备份,再将老版本的数据删除掉,将备份的数据再复制到新的版本的版本表中,定义了一个MigrationHelper类,然后在自定义GreenDaoOpenHelper中的onUpgrade()方法中调用MigrationHelper.migrate(),参数加上我们定义的Dao.class就可以了,GreenDaoOpenHelper类如下:

public class GreenDaoOpenHelper extends DaoMaster.OpenHelper {


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

    @Override
    public void onUpgrade(SQLiteDatabase 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
        }, UserDao.class, CustomerDao.class, StudentDao.class);
    }
}

升级中如果有新表的创建和删除,我们也只需要在后面增加或者删除里面Dao文件就OK了。

1.5 数据库存储路径设置

通过了解DaoMaseter.OpenHelper的源码发现,他的路径读取是通过Context的getDataBasePath(String DBName)实现的,如下:

greendao根据id更新数据 greendao数据库_greendao根据id更新数据_04

再点进去,发现这个方法是抽象的,如下:

greendao根据id更新数据 greendao数据库_Android_05

所以,我们只需要重写这个抽象方法就可以对他的存储路径进行设置了。自己封装了一个设置路径的工具类DBPathUtil,其思路是优先将路径设置在外部存储的私有目录里面,如果没有则设置到系统目录里,因为这样应用删除,其数据库也会一并删除,DBPathUtil类如下:

public final class DBPathUtil {

    private static final String EXTERNAL_STORAGE_PERMISSION = "android.permission.WRITE_EXTERNAL_STORAGE";

    private DBPathUtil() {
    }

    /**
     * 数据库文件缓存在SD卡私有目录或者系统目录,应用被删除后此方法生成的文件也被删除
     */
    public static File getIndividualCacheDirectory(Context context, String cacheDir, String dbName) {
        File appCacheDir = getCacheDirectory(context);
        File individualCacheDir = new File(appCacheDir, cacheDir);
        File dbPath=new File(individualCacheDir,dbName);
        if (!individualCacheDir.exists()) {//定义数据库的文件包目录
            if (!individualCacheDir.mkdir()) {
                return null;
            }
        }
        if(!dbPath.exists()){//创建数据库文件
            try {
                if(!dbPath.createNewFile()){
                    return null;
                }
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }
        return dbPath;
    }

    private static File getCacheDirectory(Context context) {
        return getCacheDirectory(context, true);
    }

    private static File getCacheDirectory(Context context, boolean preferExternal) {
        File appCacheDir = null;
        String externalStorageState;
        try {
            externalStorageState = Environment.getExternalStorageState();
        } catch (NullPointerException e) { // (sh)it happens (Issue #660)
            externalStorageState = "";
        } catch (IncompatibleClassChangeError e) { // (sh)it happens too (Issue #989)
            externalStorageState = "";
        }
        // 外部存储私有缓存目录 /storage/emulated/0/Android/data/com.john.testproject/cache
        if (preferExternal && MEDIA_MOUNTED.equals(externalStorageState) && hasExternalStoragePermission(context)) {
            appCacheDir = getExternalCacheDir(context);
        }
        //内部缓存目录 /data/data/com.john.testproject/cache
        if (appCacheDir == null) {
            appCacheDir = context.getCacheDir();
        }
        //内部缓存目录 /data/data/com.john.testproject/cache 和上一种差不多 之前有/ 现在没有了
        if (appCacheDir == null) {
            String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache";
            appCacheDir = new File(cacheDirPath);
        }
        return appCacheDir;
    }
    /**
     * 外部存储私有缓存目录
     */
    private static File getExternalCacheDir(Context context) {
        File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data");
        File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache");
        if (!appCacheDir.exists()) {
            if (!appCacheDir.mkdirs()) {
                return null;
            }
            try {
                new File(appCacheDir, ".nomedia").createNewFile();
            } catch (IOException e) {
            }
        }
        return appCacheDir;
    }

    private static boolean hasExternalStoragePermission(Context context) {
        int perm = context.checkCallingOrSelfPermission(EXTERNAL_STORAGE_PERMISSION);
        return perm == PackageManager.PERMISSION_GRANTED;
    }
}

然后写一个设置存储的类,GreenDaoContextHelper继承ContextWrapper,重写getDatabasePath()就OK了,如下:

public class GreenDaoContextHelper extends ContextWrapper {

    private Context context;
    public GreenDaoContextHelper(Context context) {
        super(context);
        this.context = context;
    }

    /**
     *  数据库文件存储路径 /mnt/internal_sd/Android/data/com.john.testproject/cache/John_database/test-db
     */
    @Override
    public File getDatabasePath(String name) {
        File dbFile= DBPathUtil.getIndividualCacheDirectory(context,"John_database",name);
        if(dbFile!=null){
            return dbFile;
        }else {
            return super.getDatabasePath(name);
        }

    }

1.6 使用GreenDao遇到的坑

1.6.1  创建一个实体类,如User类,在其中必须要通过@Id指定一个id字段,且该字段必须是long或者是Long类型的(网上有人说必须是Long类型的,是不对的,经实测基本类型的long类型也可以)。需要多说一点的是,编译后会在该类的构造方法上自动添加@Generated的注解,并且生成一个hashcode,此外还会自动生成一个带有@Generated注解和hashcode的无参的构造方法,如果对该类进行修改(如修改了参数名或参数类型),必须要删除带参的构造方法上的hashcode,重新编译让它自动生成一个新的code。无参的没关系,我的理解是无参的构造方法没有改变,所以hashcode可以是用原来的。还有一点要说明的是指定的id字段如果没有被

@Property(nameInDb = "my_id")注解,则在数据库中会默认保存成_id列名,不管你在该类中给它命名为什么。

1.6.2  升级数据库新添加int类型字段的时候,数据迁移报错了,查看到是因为新增int、long类型字段的时候,给的是NOT NULL,就是说不能为空。

数据库升级方案思路是:

1、新建一张跟原先表一样的临时表,同时把数据也复制进去。

2、删除原先表

3、新建最新的表,将临时表数据复制进去

4、删除临时表

问题就出在第三不,如果临时表有1个字段。新表有2个字段,而且新多的字段为int类型,那么复制数据的时候,字段二(新增)是存NULL,但是其属性是不能为NULL,这就异常了。解决思路:在临时表上也给多加一个一样的字段,然后给默认值,因为有了默认值,所有当复制数据的时候就不会是NULL,亲测,可行。

 

GreenDao的基本使用介绍的差不多了,各位看官有好的建议或者意见欢迎提出来相互交流,谢谢了。