前言
原生数据库直接操作不好用吗?
为什么要使用GreenDao?
GreenDao的优势又在哪里?
长久不用数据库,都已经生疏了,本文接下来将对GreenDao 在使用层进行分析,没有 GreenDao 模板配套生成静态代码等的原理分析。
目录
一、SQLite
SQLite是一个轻量级、跨平台的关系型数据库。
使用原生数据库SQLite,需要借助Android提供的辅助类 SQLiteOpenHelper,继承自它就可以实现数据库的创建、升级、更新数据库时的某些操作等等。前提是你需要对SQL语句有一定的了解 SQLite教程
优点:
-
轻量级 体积小
-
无需安装
-
跨平台/可移植
-
数据库信息在一个文件内
缺点:
-
并发场景,数据库可能会被写操作独占,从而导致其它读写操作阻塞或出错
-
SQL标准支持不全
使用注意事项:
-
Sqlite 写操作 不是线程安全的
-
Sqlite 读操作是线程安全的
-
插入大量数据时最好开启事务,避免单独循环插入,多次读写磁盘,Sqlite默认一个插入语句是一个事务
-
使用execSQL方法可能会导致注入攻击
对于SQL语句的执行,可以用 SQLiteDatabase的execSQL() 方法,也可以用ContentValues来实现插入、修改数据等。
在查询时,我们可能需要写类似如下代码
List<Person> result = new ArrayList<>();
Cursor cursor = null;
SQLiteDatabase readableDatabase = mDbHelper.getReadableDatabase();
try {
cursor = readableDatabase.rawQuery("select * from person where age > ? order by age desc", new String[]{"18"});
while (cursor.moveToNext()) {
String id = cursor.getString(cursor.getColumnIndex("_id"));
String name = cursor.getString(cursor.getColumnIndex("NAME"));
String sex = cursor.getString(cursor.getColumnIndex("SEX"));
String age = cursor.getString(cursor.getColumnIndex("AGE"));
Person person = new Person(id,name,sex,age);
result.add(person);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭游标
if (cursor != null) {
cursor.close();
cursor = null;
}
//关闭数据库
readableDatabase.close();
}
我们在解析数据时,需要操作游标然后遍历匹配,构建对象,返回数据集合,这对于应用层开发人员来说,不是很友好。
二、GreenDao
GreenDao是一个对象关系映射型框架,提供一个接口通过操作对象的方法去操作关系型数据库。
1、优缺点
优点:
-
性能高,Android最快的关系型数据库,存取速度快
-
依赖体积小
-
内存开销小
-
支持SQLCipher加密
-
强大的社区支持
缺点:
-
需要获取Dao类,修改实体,需要重新Rebuild
-
默认数据库升级会删除所有的表,重新建立,这里需要注意适配老版本的APP,检查数据库类型是否有变更
2、简单使用
-
导入
主工程
build.gradleclasspath 'org.greenrobot:greendao-gradle-plugin:3.3.0'模块
build.gradleapply plugin: 'org.greenrobot.greendao' dependencies { api 'org.greenrobot:greendao:3.2.0' } -
编写实体类
Custom.class@Entity public class Custom { @Id(autoincrement = true) private Long id; private String shopName; private String shopDescription; }Rebuild或者 同步一下 Custom 类如下@Entity public class Custom { @Id(autoincrement = true) private Long id; private String shopName; private String shopDescription; @Generated(hash = 567200405) public Custom(Long id, String shopName, String shopDescription) { this.id = id; this.shopName = shopName; this.shopDescription = shopDescription; } @Generated(hash = 62298964) public Custom() { } public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public String getShopName() { return this.shopName; } public void setShopName(String shopName) { this.shopName = shopName; } public String getShopDescription() { return this.shopDescription; } public void setShopDescription(String shopDescription) { this.shopDescription = shopDescription; } }并且对应目录会生成此类文件
-
获取并使用Dao类
示例 增、删、更新、查找
DBManager.classprivate DBManager(){ if(openHelper == null) { openHelper = new DaoMaster.DevOpenHelper(Global.getInstance().getApplicationContext(), "custom_db"); SQLiteDatabase sqLiteDatabase = openHelper.getWritableDatabase(); daoSession = new DaoMaster(sqLiteDatabase).newSession(); } } public DaoSession getDaoSession(){ return daoSession; }DBOperationInterfacepublic interface DBOperationInterface<T> { void insert(List<T> data); void delete(List<T> data); void update(List<T> data); List<T> find(Object... args); }CustomOperationpublic class CustomOperation extends BaseOperation<Custom> { //增 @Override public void insert(List<Custom> data) { if (data == null || data.isEmpty()) { return; } getDaoSession().getCustomDao().saveInTx(data); } //删 @Override public void delete(List<Custom> data) { if (data == null || data.isEmpty()) { return; } getDaoSession().getCustomDao().deleteInTx(data); } //更新 @Override public void update(List<Custom> data) { if (data == null || data.isEmpty()) { return; } getDaoSession().getCustomDao().updateInTx(data); } //查找 @Override public List<Custom> find(Object... args) { if (args == null || args.length == 0) { return getDaoSession().getCustomDao().loadAll(); } QueryBuilder<Custom> queryBuilder = getDaoSession().getCustomDao().queryBuilder(); if (args.length == 1 && args[0] instanceof String) { return queryBuilder.where(CustomDao.Properties.ShopName.eq(args[0])).list(); } else if (args.length == 2 ) { if (args[0] instanceof String && args[1] instanceof String) { return queryBuilder.where(CustomDao.Properties.ShopName.eq(args[0]), CustomDao.Properties.ShopDescription.eq(args[1])).list(); } } return null; } }
3、原理分析
以下将对GreenDao使用原理进行分析,由于插入和更新、删除差不多,这里仅对插入fe
3.1、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);
}
}
构造函数调用的是父类OpenHelper的构造函数,在 onUpgrade 时会先删除所有表然后再调用父类onCreate 重新 新建表,如果不想这么操作,可以自定义一个类继承OpenHelper来替代官方默认的DevOpenHelper。
OpenHelper是继承自DatabaseOpenHelper的,在此类中,有类似方法需要注意一下,这里内部代表了两种不同类型的数据库,分别是StandardDatabase 和 EncryptedDatabase,前者没有加密,后者使用了SQLCipher加密
public Database getWritableDb() {
return wrap(getWritableDatabase());
}
public Database getEncryptedWritableDb(String password) {
EncryptedHelper encryptedHelper = checkEncryptedHelper();
return encryptedHelper.wrap(encryptedHelper.getReadableDatabase(password));
}
3.2、DaoSession 的创建
相关过程图如下
DaoSession创建过程中会先构造DaoMaster ,通过传递DataBase 或者是 SQLiteDatabase ,如果传递的是android系统的SQLiteDataase,构造函数内将会对它包装一下,使用StandardDatabase 来代理 SQLiteDatabase
public DaoMaster(SQLiteDatabase db) {
this(new StandardDatabase(db));
}
public DaoMaster(Database db) {
super(db, SCHEMA_VERSION);
registerDaoClass(CustomDao.class);
}
在DaoMaster的构造方法里,还有通过 super 调用父类AbstractDaoMaster的构造函数,初始化HashMap daoConfigMap ,
然后通过接下来的 registerDaoClass(CustomDao.class) 来构建DaoConfig对象 (这里会创建一个跟SQL语句执行相关的TableStatements类) ,并且存放daoClass和DaoConfig的映射关系。
构造完DaoMaster类后,通过调用其**newSession**方法返回DaoSeesion类
public DaoSession newSession(IdentityScopeType type) {
return new DaoSession(db, type, daoConfigMap);
}
DaoSession的构造函数里做了些什么呢?
public DaoSession(Database db, IdentityScopeType type, Map<Class<? extends AbstractDao<?, ?>>, DaoConfig>
daoConfigMap) {
super(db);
customDaoConfig = daoConfigMap.get(CustomDao.class).clone();
customDaoConfig.initIdentityScope(type);
customDao = new CustomDao(customDaoConfig, this);
registerDao(Custom.class, customDao);
}
protected <T> void registerDao(Class<T> entityClass, AbstractDao<T, ?> dao) {
entityToDao.put(entityClass, dao);
}
初始化了entityToDao,生成了customDaoConfig对象,并存放了Custom.class与CustomDao的映射关系到entityToDao中,还有customDaoConfig 也对IdentityScope进行了初始化
public void initIdentityScope(IdentityScopeType type) {
if (type == IdentityScopeType.None) {
identityScope = null;
} else if (type == IdentityScopeType.Session) {
if (keyIsNumeric) {
identityScope = new IdentityScopeLong();
} else {
identityScope = new IdentityScopeObject();
}
} else {
throw new IllegalArgumentException("Unsupported type: " + type);
}
}
这个IdentityScope 用于存放ID和实体类映射关系的缓存。
3.3、插入
原理图
分析
public void saveInTx(Iterable<T> entities) {
int updateCount = 0;
int insertCount = 0;
//注释 1
for (T entity : entities) {
if (hasKey(entity)) {
updateCount++;
} else {
insertCount++;
}
}
if (updateCount > 0 && insertCount > 0) {
//注释 2
List<T> toUpdate = new ArrayList<>(updateCount);
List<T> toInsert = new ArrayList<>(insertCount);
for (T entity : entities) {
if (hasKey(entity)) {
toUpdate.add(entity);
} else {
toInsert.add(entity);
}
}
db.beginTransaction();
try {
updateInTx(toUpdate);
insertInTx(toInsert);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} else if (insertCount > 0) {
//注释 3
insertInTx(entities);
} else if (updateCount > 0) {
updateInTx(entities);
}
}
注释 1 处 会判断 需要更新的和需要插入的数据的数量,如果都有,则走注释 2,注释 2 内会根据 hasKey 方法 判断 该实体类对象有没有 ID,有ID则 hasKey 返回 true,然后开启事务,更新数据、插入数据。但是作为插入调用 saveInTx 方法一般都是直接走 注释 3 处,调用 insertInTx 方法
public void insertInTx(Iterable<T> entities) {
insertInTx(entities, isEntityUpdateable()); // isEntityUpdateable 返回的是 固定的 true
}
public void insertInTx(Iterable<T> entities, boolean setPrimaryKey) {
DatabaseStatement stmt = statements.getInsertStatement();
executeInsertInTx(stmt, entities, setPrimaryKey);
}
最关键的是 executeInsertInTx 方法
private void executeInsertInTx(DatabaseStatement stmt, Iterable<T> entities, boolean setPrimaryKey) {
db.beginTransaction();
try {
synchronized (stmt) {
if (identityScope != null) {
identityScope.lock(); // 上锁 保证读写正常
}
try {
if (isStandardSQLite) {
//注释 1
SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement();
for (T entity : entities) {
//注释 2
bindValues(rawStmt, entity);
if (setPrimaryKey) {
//注释 3
long rowId = rawStmt.executeInsert();
//注释 4
updateKeyAfterInsertAndAttach(entity, rowId, false);
} else {
rawStmt.execute();
}
}
} else {
......
}
} finally {
if (identityScope != null) {
identityScope.unlock();
}
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
我们使用的是不加密的数据库,所以 isStandardSQLite 是 true, 注释 1 处 是通过 stmt 得到 Android API中的SQLiteStatement对象,然后注释 2 处,为SQLiteStatement 设置参数值,注释 3 处执行插入语句,得到 ID 值,然后 注释 4 处会为entity实体类设置ID,并调用 attachEntity 将 ID和实体类关系映射存放在 IdentityScope 中。bindValues 和 attachEntity 方法如下
protected final void bindValues(SQLiteStatement stmt, Custom entity) {
stmt.clearBindings();
Long id = entity.getId();
if (id != null) {
stmt.bindLong(1, id);
}
String shopName = entity.getShopName();
if (shopName != null) {
stmt.bindString(2, shopName);
}
String shopDescription = entity.getShopDescription();
if (shopDescription != null) {
stmt.bindString(3, shopDescription);
}
}
3.4、查找
原理图
分析
我们这里分析下 loadAll 方法
public List<T> loadAll() {
Cursor cursor = db.rawQuery(statements.getSelectAll(), null);
return loadAllAndCloseCursor(cursor);
}
protected List<T> loadAllAndCloseCursor(Cursor cursor) {
try {
return loadAllFromCursor(cursor);
} finally {
cursor.close();
}
}
最终调用的是 loadAllAndCloseCursor 方法
protected List<T> loadAllFromCursor(Cursor cursor) {
int count = cursor.getCount();
if (count == 0) {
return new ArrayList<T>();
}
List<T> list = new ArrayList<T>(count);
CursorWindow window = null;
boolean useFastCursor = false;
//注释 1
if (cursor instanceof CrossProcessCursor) {
window = ((CrossProcessCursor) cursor).getWindow();
if (window != null) { // E.g. Robolectric has no Window at this point
if (window.getNumRows() == count) {
cursor = new FastCursor(window);
useFastCursor = true;
} else {
DaoLog.d("Window vs. result size: " + window.getNumRows() + "/" + count);
}
}
}
if (cursor.moveToFirst()) {
if (identityScope != null) {
identityScope.lock();
identityScope.reserveRoom(count);
}
try {
//注释 2
if (!useFastCursor && window != null && identityScope != null) {
loadAllUnlockOnWindowBounds(cursor, window, list);
} else {
// 注释 3
do {
list.add(loadCurrent(cursor, 0, false));
} while (cursor.moveToNext());
}
} finally {
if (identityScope != null) {
identityScope.unlock();
}
}
}
return list;
}
注释 1 处 会判断 Cursor 是不是跨进程的,并且如果 numRows == count 没有偏差,就创建FastCurosr 进行遍历,然后会走注释 3 处,否则走注释 2 处。注释 3 处数据收集 是通过 loadCurrent 方法,其实 注释 2 处也是
private void loadAllUnlockOnWindowBounds(Cursor cursor, CursorWindow window, List<T> list) {
int windowEnd = window.getStartPosition() + window.getNumRows();
for (int row = 0; ; row++) {
list.add(loadCurrent(cursor, 0, false));
row++;
......
if (!cursor.moveToNext()) {
break;
......
}
}
所以我们直接分析 loadCurrent 方法即可
final protected T loadCurrent(Cursor cursor, int offset, boolean lock) {
//注释 1
if (identityScopeLong != null) {
if (offset != 0) {
// Occurs with deep loads (left outer joins)
if (cursor.isNull(pkOrdinal + offset)) {
return null;
}
}
long key = cursor.getLong(pkOrdinal + offset);
//注释 2
T entity = lock ? identityScopeLong.get2(key) : identityScopeLong.get2NoLock(key);
if (entity != null) {
return entity;
} else {
//注释 3
entity = readEntity(cursor, offset);
attachEntity(entity);
if (lock) {
identityScopeLong.put2(key, entity);
} else {
identityScopeLong.put2NoLock(key, entity);
}
return entity;
}
}
.....
}
由于我们创建的实体类 Custom 中 ID 是 long类型,所以 注释 1 处 identityScopeLong不为空。
还记得之前分析插入数据时 ,提到过会将 ID 和实体类关系映射存放到 IdentityScope 中吗?注释2处就是先从这个 IdentityScope 中找数据,没有的话,走到注释 3 处,调用 readEntity 读取数据
@Override
public Custom readEntity(Cursor cursor, int offset) {
Custom entity = new Custom( //
cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), // id
cursor.isNull(offset + 1) ? null : cursor.getString(offset + 1), // shopName
cursor.isNull(offset + 2) ? null : cursor.getString(offset + 2) // shopDescription
);
return entity;
}
三、总结
SQLite是一个关系型数据库,在代码中使用 需要 自己手动实现 创建 游标进行查询、删除等操作代码,操作很繁琐。
而作为对象关系映型框架GreenDao,它将大部分我们需要自己实现的代码都封装好了
-
针对不同SQL语句,都结合了代理模式进行封装
-
针对查找还具备缓存功能,
-
提供了SQLCipher加密和不加密两种方式,使用及其方便
-
插入数据时间 都差不多,但是上万条数据的删除和更新,快 1 比 40 了
如有错误,请指出,谢谢!