数据库操作是Android开发中的重要部分,通常我们不直接使用SDK中的Sqlite API(难度大,开发效率低,当然运行效率是最快的),而是使用第三方的ORM框架,如 GreenDao。GreenDao可以极大地提高建库,升级,增,删,改,查等工作的效率。虽然有GreenDao,如果不能很好地组织数据表和数据表的操作,随着业务的增加,依然会有很大的困扰。有没有一个套路,写起来代码结构清晰,重复代码少,方便扩展,调用起来也方便简单?这就是本文要写的内容,围绕着这个问题的一次实践。代码的GitHub地址为:GreenDaoImpl
根据经验,把数据表,数据表操作,数据库升级等DB相关的代码组织在一个包下可以更方便的查找与维护。把数据和数据的操作统一组织起来,作为数据源提供数据的存取,符合了模块化设计的思想。所以,把GreenDao封装在db包下,如图所示:
db包下有四个类:AbstractDaoHandler,DaoManager,MyDataase,MyOpenHelper;五个二级包名:converter,dao,schema,typedef,upgrade。
这个代码组织结构考虑了GreenDao提供的接口:创建实体,数据表操作,自定义类型,数据表升级。如果需要扩展新的数据表,或者增加新的表操作,只需要横向增加即可。数据库,数据表定义,数据表接口,接口管理,数据升级一眼看去基本能了解个大概。在减少重复代码的基础上,增加了代码的可阅读性,可扩展性。
如何定义一张数据表,详情请看 Modeling enties,这里不做过多的描述,简单记录下需要注意的事项。
如果要增加一张新数据表Xyz,在schema包下新建一个Xyz类,定义其表结构。然后在dao包中增加一个XyzDaoProxy类,继承自AbstractDaoHandler,最后注册在DaoManager。这样就可以使用就可以通过DaoManager获取到这个XyzDaoProxy,然后就可以对Xyz这个数据表进行增删改查等操作了。
查看GreenDao生成的代码中,有一个抽象类:AbstractDao
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里面,支持级联删除等。