如何优雅地使用GreenDao

前言

数据库操作是Android开发中的重要部分,通常我们不直接使用SDK中的Sqlite API(难度大,开发效率低,当然运行效率是最快的),而是使用第三方的ORM框架,如 GreenDao。GreenDao可以极大地提高建库,升级,增,删,改,查等工作的效率。虽然有GreenDao,如果不能很好地组织数据表和数据表的操作,随着业务的增加,依然会有很大的困扰。有没有一个套路,写起来代码结构清晰,重复代码少,方便扩展,调用起来也方便简单?这就是本文要写的内容,围绕着这个问题的一次实践。代码的GitHub地址为:GreenDaoImpl

代码结构

根据经验,把数据表,数据表操作,数据库升级等DB相关的代码组织在一个包下可以更方便的查找与维护。把数据和数据的操作统一组织起来,作为数据源提供数据的存取,符合了模块化设计的思想。所以,把GreenDao封装在db包下,如图所示:

如何优雅地使用GreenDao_第1张图片
db包下有四个类:AbstractDaoHandler,DaoManager,MyDataase,MyOpenHelper;五个二级包名:converter,dao,schema,typedef,upgrade。

  1. schema 顾名思义,可以理解为数据库,这个包名下每一个类都代表一个数据表;
  2. dao ,这个包下的每一个类都继承自 AbstractDaoHandler, 对应操作schema里的一个数据表,数据表的所有操作都写在XxxDaoProxy类(基本的增删改查在抽象类中实现了),内部的实现就是我们的GreenDao 生成的相应的 Dao类,每增加一个数据表,对应在dao下增加一个DaoProxy;
  3. typedef,这个包下是数据表中的自定义数据类型,与converter对应;
  4. converter,对应typedef,实现自定义的类型与基本类型相互转化;
  5. upgrade,记录每个版本变更;
  6. AbstractDaoHandler,数据表操作的基类,封装了基本的增删改查操作,只要继承这个基类,传递一个数据表类型,无须添加任何代码,即可拥有对数据表增删改查的能力;
  7. DaoManager 管理dao下的所有类,是一个单例类,DaoManager 对象获取对应的XyzDaoProxy,每增加一个DaoProxy都在这里增加一个DaoProxy对象,统一管理;
  8. MyDatabase 这里创建Greendao的Database和DaoSession的实例,在Application中初始化后就可以获取实例;
  9. MyOpenHelper 数据表升级,和upgrade中的类一起使用,管理每一个版本的变更。

根据类的关系,下面是大概的类图:
如何优雅地使用GreenDao_第2张图片

这个代码组织结构考虑了GreenDao提供的接口:创建实体,数据表操作,自定义类型,数据表升级。如果需要扩展新的数据表,或者增加新的表操作,只需要横向增加即可。数据库,数据表定义,数据表接口,接口管理,数据升级一眼看去基本能了解个大概。在减少重复代码的基础上,增加了代码的可阅读性,可扩展性。

数据表结构定义

如何定义一张数据表,详情请看 Modeling enties,这里不做过多的描述,简单记录下需要注意的事项。

  1. 最理想的状态是数据表结构一旦定义好,就不会发生变化(论开发初期定义好数据结构的重要性),实际随着版本升级,数据表结构可能会发生变化,需要改数据表的时候,升级 schemaVersion,记录好哪个app版本对应哪个schemaVersion,有哪些变化,这个对后续app维护特别重要;
  2. 在设计数据表的时候,每个字段的类型,名称,唯一性,能否为空等,要考虑清楚;
  3. 在设计数据表的时候,不要和后台 API 返回的实体类型混为一体;
  4. Greendao 不支持联合主键,但是可以通过 indexes 解决此问题,参考此文;
  5. 存储自定义类型,可以通过 PropertyConverter 接口来实现;
  6. GreenDao支持一对一,一对多的关系,但是并不支持级联删除

如果要增加一张新数据表Xyz,在schema包下新建一个Xyz类,定义其表结构。然后在dao包中增加一个XyzDaoProxy类,继承自AbstractDaoHandler,最后注册在DaoManager。这样就可以使用就可以通过DaoManager获取到这个XyzDaoProxy,然后就可以对Xyz这个数据表进行增删改查等操作了。

Dao封装

查看GreenDao生成的代码中,有一个抽象类:AbstractDao,这个类封装了数据表的操作。每一个对应的Dao类,都继承自这个类。所以,可以利用这个类,创建我们的AbstractDaoHandler,其中T就是对应的实体类,然后AbstractDaoHandler通过构造函数,注入一个AbstractDao 对象,这个对象就是GreenDao生成的XyzDao。


import org.greenrobot.greendao.AbstractDao;
import org.greenrobot.greendao.query.QueryBuilder;

import java.util.ArrayList;
import java.util.List;

public abstract class AbstractDaoHandler<T>{

    protected AbstractDao<T, Long> dao;

    public AbstractDaoHandler(AbstractDao<T, Long> dao) {
        this.dao = dao;
    }

    public long insert(T data) {
        return dao.insert(data);
    }

    public void insertInTx(List<T> data) {
        dao.insertInTx(data);
    }

    public void update(T data) {
        dao.update(data);
    }

    public void updateInTx(List<T> data) {
        dao.updateInTx(data);
    }

    public long insertOrReplace(T data) {
        return dao.insertOrReplace(data);
    }

    public void insertOrReplaceInTx(List<T> data) {
        dao.insertOrReplaceInTx(data);
    }

    public T loadByRowId(Long id) {
        return dao.loadByRowId(id);
    }

    public List<T> loadAll() {
        return dao.loadAll();
    }

    public void deleteByKey(long id) {
        dao.deleteByKey(id);
    }

    public void delete(T data) {
        dao.delete(data);
    }

    public void deleteInTx(List<T> data) {
        dao.deleteInTx(data);
    }

    public void deleteAll() {
        dao.deleteAll();
    }

    public QueryBuilder<T> queryBuilder() {
        return dao.queryBuilder();
    }

    public List<T> query(QueryBuilder<T> queryBuilder) {
        List<T> result = null;
        try {
            result = queryBuilder.list();
        } catch (Exception e) {
            e.printStackTrace();
        }
        if(result == null) {
            result = new ArrayList<>();
        }
        return result;
    }

    public T unique(QueryBuilder<T> queryBuilder) {
        T result = null;
        try {
            result = queryBuilder.unique();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    public abstract T unique(T data);

}

数据库升级

数据库升级是一件不可避免的事情。关于数据库升级,有两个事实,一,只能从低版本升级到高版本,而不能从高版本降级;二,由于app版本升级的原因,可能会出现从版本1直接升级到版本3,而跳过版本2,如果不提供1到3的直接升级,只能从1升级到2,然后再从2升级到3。我们记录每一个版本的变更,然后根据oldVersion可以判断需要执行哪几个版本的升级,如oldVersion=3,但是newVersion=5,那么有3-4,4-5两个版本要升级。定义一个抽象基类,一个抽象方法upgrade(),每个版本升级,就继承自这个类,并实现upgrde():


import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;
import android.util.Log;

/**
 * 通过比较versionCode与oldVersion,如果 oldVersion <= versionCode,则需要升级。
 */
public abstract class Migration {

    /**
     * 这个版本号可以理解是上一个版本,如当前将要发布的版本是2,上一个版本是1,则versionCode=1;
     * 以此类推,versionCode=1,2,3...(假定versionCode每次升级都自增1)
     */
    public final int versionCode;

    public Migration(int versionCode) {
        this.versionCode = versionCode;
    }

    public void migrate(SQLiteDatabase db, int oldVersion) {
        if(oldVersion <= versionCode) {
            upgrade(db);
        }
    }

    protected String addColumn(String tableName, String columnName, String type, boolean nullAble, String defaultVal) {
        StringBuilder addColumn = new StringBuilder();
        addColumn.append("ALTER TABLE ").append(tableName)
                .append(" ADD COLUMN ").append(columnName).append(" ").append(type);
        if(!nullAble) {
            addColumn.append(" NOT NULL DEFAULT(").append(defaultVal).append(");");
        } else {
            addColumn.append(";");
        }
        String sql = addColumn.toString();
        Log.d("MyOpenHelper", sql);
        return sql;
    }

    protected void alterColumnNullAble(String tableName, String columnName, String type, boolean nullAble, String defaultVal) {
        // ALTER TABLE 表 ALTER COLUMN [字段名] 字段类型 NOT NULL
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("ALTER TABLE ").append(tableName)
                .append(" ALTER COLUMN ").append(columnName).append(" ").append(type);
        if(nullAble) {
            stringBuilder.append(" NULL;");
        } else {
            stringBuilder.append(" NOT NULL DEFAULT(").append(defaultVal).append(");");
        }

    }

    protected boolean hasColumn(SQLiteDatabase db, String tableName, String column) {
        if (TextUtils.isEmpty(tableName) || TextUtils.isEmpty(column)) {
            return false;
        }
        Cursor cursor = null;
        try {
            cursor = db.query(tableName, null, null,
                    null, null, null, null);
            if (null != cursor && cursor.getColumnIndex(column) != -1) {
                return true;
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }
        return false;
    }

    protected abstract void upgrade(SQLiteDatabase db);
}

记录了每个版本的变更,需要在MyOpenHelper 中调用升级,注意升级时有顺序的,必须保证从低到高,逐个版本加入(Migration2,MIgration3…):


import android.content.Context;
import android.database.sqlite.SQLiteDatabase;


import com.bottle.alive.db.upgrade.Migration;
import com.bottle.alive.gen.DaoMaster;

import org.greenrobot.greendao.database.Database;
import org.greenrobot.greendao.database.StandardDatabase;

import java.util.ArrayList;
import java.util.List;

/**
 * 数据表升级,配合app/build.gradle 中的 greendao#schemaVersion使用,升级数据库中的数据表(如增加column等操作)
 * 建议:1.开始设计时要尽量考虑未来的需求,避免改动;
 *       2.如果要修改表,尽量新增column,而不要删除column,也不要修改column的类别
 *       3.没必要迁移整张表的数据,考虑使用SQL 新增column或者修改column
 *       4.版本名称从1开始,每次升级增加1,并且记录每个版本的变化
 */
public class MyOpenHelper extends DaoMaster.OpenHelper {

    private List<Migration> migrations;

    public MyOpenHelper(Context context, String name) {
        super(context, name);
    }

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

    @Override
    public void onCreate(Database db) {
        super.onCreate(db);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        super.onUpgrade(db, oldVersion, newVersion);
        if(migrations == null) {
            // 如果没有提供升级的对象,那么删除所有的表,然后再重新建表
            DaoMaster.dropAllTables( new StandardDatabase(db), true);
            onCreate(db);
        } else {
            for (Migration migration : migrations) {
                migration.migrate(db, oldVersion);
            }
        }
    }

    /**
     * 按照从低到高的版本顺序排列
     * @param migrations 每升级一个版本就新建一个Migration类
     */
    public void setMigrations(Migration... migrations) {
        if(migrations == null || migrations.length == 0) {
            return;
        }
        if(this.migrations == null) {
            this.migrations = new ArrayList<>();
        }
        this.migrations.clear();
        for(Migration migration : migrations) {
            this.migrations.add(migration);
        }
    }
}

关于升级,刚开始搜索到一个项目GreenDaoUpgradeHelper,它主要是通过创建一个临时表,将旧表的数据迁移到新表。但是觉得没必要创建临时表并迁移数据,然后删除老表,再新建,最后将临时表中的数据写入到新表。通过执行一个SQL,直接对数据表进行修改,可以避免这么复杂的流程。所以,没有尝试使用这个项目的代码。

总结

GreenDao 可以大大提高SQLite数据的开发效率。本文总结了GreenDao创建数据表,封装数据表操作,数据表升级的一些实战经验,从而更好地掌控App的数据模型。最近在看Android的Jetpack,有一个类似的组件Room,感觉可以替代GreenDao。因为它支持很多GreenDao不支持的特性,如Room支持把代码单独写在一个Module里面,支持级联删除等。

你可能感兴趣的:(android,工具)