介绍下StorIOSQLite到RxJava

storio

storio是github上的开源项目,它用来操作SQLiteDatabaseContentResolver的操作进行封装,提供更简单更强大的api.我们项目最近从xutils3转换为StorIOSQLite,说说对它的体验和它是如何使用RxJava进行封装.

storio是通过预先代码生成进行实体类的赋值

通过SQLiteTypeMapping提供实体的三种Resolver:

  1. PutResolver
  2. GetResolver
  3. DeleteResolver

也就是分别对应我们在进行API操作时

  1. 将实体转换为数据库数据(InsertQuery,UpdateQuery…)
  2. 将数据库数据(Cursor)装换成实体对象
  3. 如何执行删除操作

常规的Resolver只要我在实体类中使用它的Annotation Processor便会自动生成。

什么时候需要自定义呢?

复杂实体对象的时候(连表查询),因为storio是不关心具体实体类的,它只关心如何将实体和数据库操作建立起关系
比如文档中的UserWithTweetsGetResolver.java

借鉴xutils利用HashMap来定义通用的读取实体

DbModel.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import android.text.TextUtils;
import java.util.Date;
import java.util.HashMap;
public final class DbModel {
/**
* key: columnName
* value: valueStr
*/
private HashMap<String, String> dataMap = new HashMap<String, String>();
public String getString(String columnName) {
return dataMap.get(columnName);
}
public int getInt(String columnName) {
return Integer.valueOf(dataMap.get(columnName));
}
public boolean getBoolean(String columnName) {
String value = dataMap.get(columnName);
if (value != null) {
return value.length() == 1 ? "1".equals(value) : Boolean.valueOf(value);
}
return false;
}
public double getDouble(String columnName) {
return Double.valueOf(dataMap.get(columnName));
}
public float getFloat(String columnName) {
return Float.valueOf(dataMap.get(columnName));
}
public long getLong(String columnName) {
return Long.valueOf(dataMap.get(columnName));
}
public Date getDate(String columnName) {
long date = Long.valueOf(dataMap.get(columnName));
return new Date(date);
}
public java.sql.Date getSqlDate(String columnName) {
long date = Long.valueOf(dataMap.get(columnName));
return new java.sql.Date(date);
}
public void add(String columnName, String valueStr) {
dataMap.put(columnName, valueStr);
}
/**
* @return key: columnName
*/
public HashMap<String, String> getDataMap() {
return dataMap;
}
/**
* @param columnName
* @return
*/
public boolean isEmpty(String columnName) {
return TextUtils.isEmpty(dataMap.get(columnName));
}
}

提供SQLiteTypeMapping,只提供get操作

DbModelSQLiteTypeMapping.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import android.database.Cursor;
import android.support.annotation.NonNull;
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping;
import com.pushtorefresh.storio.sqlite.StorIOSQLite;
import com.pushtorefresh.storio.sqlite.operations.delete.DeleteResolver;
import com.pushtorefresh.storio.sqlite.operations.delete.DeleteResult;
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver;
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver;
import com.pushtorefresh.storio.sqlite.operations.put.PutResult;
/**
* Created by dannyhe on 14/01/2017.
*/
public class DbModelSQLiteTypeMapping extends SQLiteTypeMapping<DbModel> {
public static class DBModelPutResolver extends PutResolver<DbModel> {
@NonNull
@Override
public PutResult performPut(@NonNull StorIOSQLite storIOSQLite, @NonNull DbModel object) {
return PutResult.newUpdateResult(0,"");
}
}
public static class DBModelGetResolver extends DefaultGetResolver<DbModel> {
@NonNull
@Override
public DbModel mapFromCursor(@NonNull Cursor cursor) {
DbModel result = new DbModel();
int columnCount = cursor.getColumnCount();
for (int i = 0; i < columnCount; i++) {
result.add(cursor.getColumnName(i), cursor.getString(i));
}
return result;
}
}
public static class DBModelDeleteResolver extends DeleteResolver<DbModel> {
@NonNull
@Override
public DeleteResult performDelete(@NonNull StorIOSQLite storIOSQLite, @NonNull DbModel object) {
return DeleteResult.newInstance(0,"");
}
}
public DbModelSQLiteTypeMapping() {
super(new DBModelPutResolver(),
new DBModelGetResolver(),
new DBModelDeleteResolver());
}
}

如何使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
StorIOSQLite mStorIOSQLite = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(dbOpenHelper)
.defaultScheduler(defaultStorIOSQLiteScheduler()) //io thread
.addTypeMapping(DbModel.class, new DbModelSQLiteTypeMapping())
.build;
DbModel dbModel = mStorIOSQLite.get()
.object(DbModel.class)
.withQuery(RawQuery.builder()
.query("SELECT COUNT(*) AS c FROM sqlite_master WHERE type=? AND name=?")
.args("table", tableName)
.build())
.prepare()
.executeAsBlocking();

Entity VS Cursor

storio提供了两种读取方式:

  1. Entity

    1
    2
    3
    4
    5
    6
    7
    8
    DbModel dbModel = mStorIOSQLite.get()
    .object(DbModel.class)
    .withQuery(RawQuery.builder()
    .query("SELECT COUNT(*) AS c FROM sqlite_master WHERE type=? AND name=?")
    .args("table", tableName)
    .build())
    .prepare()
    .executeAsBlocking();
  2. Cursor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    final Cursor dbModelCursor = mStorIOSQLite.get()
    .cursor()
    .withQuery(RawQuery.builder()
    .query("SELECT COUNT(*) AS c FROM sqlite_master WHERE type=? AND name=?")
    .args("table", tableName)
    .build())
    .prepare()
    .executeAsBlocking();
    ...

RxJava VS executeAsBlocking

理解

关于RxJava的应用(asRxObservable()),目前数据库框架基本上都是利用PublishSubject.每个StorIOSQLite内部有个RxChangesBus,所有的asRxObservable()方法都是订阅这个bus.
订阅时会对Bus的事件过滤,只留下这次查询需要的变化(通过数据表的名称过滤)生成Observable.并通过startWith插入一次查询的结果,用来解决订阅时没有事件源触发查询操作的问题.

我们来分析下面的查询StorIOSQLite到底做了什么,下面的查询来自官网的文档:
我们先按着链式阅读来理解这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mStorIOSQLite
.get()
.listOfObjects(Tweet.class)
.withQuery(Query.builder()
.table("tweets")
.build())
.prepare()
.asRxObservable() // Get Result as rx.Observable and subscribe to further updates of tables from Query!
.observeOn(mainThread()) // All Rx operations work on Schedulers.io()
.subscribe(tweets -> { // Please don't forget to unsubscribe
// will be called with first result and then after each change of tables from Query
// several changes in transaction -> one notification
adapter.setData(tweets);
}
);
// don't forget to manage Subscription and unsubscribe in lifecycle methods to prevent memory leaks

我们拿到数据库(storIOSQLite)读取(get)存有Tweet的列表(listOfObjects(Tweet.class))通过查询(Query)数据库表”tweets”.我们需要得到Observable(asRxObservable())然后在主线程订阅(observeOn(mainThread())),最后将数据给adapter.并且后面每次tweets表发生变化都会触发这次刷新(除非取消订阅,当然为了不引发内存泄露,我们需要在合理的时候取消订阅).

下面我们再来看StorIOSQLite做了什么?

  1. 首先通过Query来获取需要查询的表,我们这里是tweets
  2. 通过内部的RxChangesBus订阅变化(Changes),并通过filter操作符过滤出存在tweets表的变化
  3. 查询一次Query并通过startWith操作符插入查询结果(会通过listOfObjects进行实体转换)

注意:一定要及时取消订阅,以免引起内存泄露

多张表的监听

比如有这样的业务需求,我需要查询多张表以完成这次查询,但是一条SQL语句又不能实现.

1
2
3
4
5
6
fetchDatasThenRefreshUI();//读取数据并刷新UI,这里会读取table1和table2
mStorIOSQLite
.observeChangesInTables(Sets.newHashSet("table1","table2"))
.subscribe(changes -> {
fetchDatasThenRefreshUI(); //如果有变化再次读取数据库
});

上面的写法是不是很繁琐,我们重构一下…

1
2
3
4
5
6
7
mStorIOSQLite
.observeChangesInTables(Sets.newHashSet("table1","table2"))
.startWith(Changes.newInstance(""))
.compose(this.<Changes>bindUntilEvent(FragmentEvent.DESTROY)) //使用RxLifecycle自动取消订阅
.subscribe(changes -> {
fetchDatasThenRefreshUI(); //读取数据并刷新UI,这里会读取table1和table2
});

主动通知变化

StorIOSQLite默认的通知变化是只要对一张表有数据操作时(CURD)均会发生通知,如果我们有个业务需求是需要给一个表里面插入1000条数据,插入成功后再刷新界面。
这个时候我们就需要LowLevel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<String> strings = ...; //存放了操作两个数据库表的sql语句
try {
mStorIOSQLite.lowLevel().beginTransaction(); //事务
for (String sql : strings) {
mStorIOSQLite.lowLevel()
.executeSQL(RawQuery.builder().query(sql).build());
}
mStorIOSQLite.lowLevel()
.setTransactionSuccessful();
//通知table1和table2发生了变化(触发再次读取事件)
mStorIOSQLite.lowLevel()
.notifyAboutChanges(Changes.newInstance(Sets.newHashSet("table1","table2")));
} finally {
mStorIOSQLite.lowLevel().endTransaction();
}