Rxjava+数据库?来用用SqlBrite和SqlDelight吧!

144
作者 CameloeAnthony
2016.07.19 17:36* 字数 3661

前言

最近大火的Rxjava可能已经应用到了不少人的项目中,上次收藏的由作者寒江不钓的两篇文章Google Play Top200 应用分析报告以及国内Top500Android应用分析报告中分别提到了使用Rxjava的应用,不乏很多优秀的大公司。



之前开源社区,也有很多关于RxJava的文章,很多公司的面试题已经是Retrofit,Rxjava这些最新技术相关的。可以预见不久之后,Rxjava定将成为每个应用的标配。

使用一段时间,相信你在进行各种操作的时候,你都会通过Rxjava来完成,直到你遇到了数据库操作,那么我们的数据库操作是否也可以利用Rxjava这种响应式,基于事件的形式的,以observable 作为操作序列的方式呢?这也是之前我刚接触rxjava的时候遇到的问题。
Square做为良心大厂,已经为我们提供了解决方案一个轻量级的库SqlBrite来实现这个需求,同时这里也将一同介绍和使用Square的一个轻量级库SqlDelight:

github地址:
square/sqlbrite
square/sqldelight


什么是SqlBrite和SqlDelight?

sqlbrite

A lightweight wrapper around SQLiteOpenHelper which introduces reactive stream semantics to SQL operations.
http://square.github.io/sqlbrite/0.x/sqlbrite/index.html

SqlBrite是对 Android 系统的 SQLiteOpenHelper 的封装,对SQL操作引入了响应式语义 (Rx)(用来在 RxJava 中使用)

sqldelight

SQLDelight generates Java models from your SQL CREATE TABLE
statements. These models give you a typesafe API to read & write the rows of your tables. It helps you to keep your SQL statements together, organized, and easy to access from Java.

SqlDelight通过从 SQL 语句来生成 JAVA 模型代码。这样的好处是,所有 SQL 语句都位于同一个位置,通过查看 SQL 语句可以清楚的了解需要实现的功能和数据库的结构,也便于管理以及java类访问。

SqlBrite的使用

以下内容来自于官方README.md文档

创建一个SqlBrite对象,该对象是该库的入口:

SqlBrite sqlBrite = SqlBrite.create();

提供一个 SQLiteOpenHelper实例和一个Scheduler实例来创建一个 BriteDatabase 对象:

BriteDatabase db = sqlBrite.wrapDatabaseHelper(openHelper, Schedulers.io());

Scheduler 是指定执行查询的操作的线程,由于查询数据库是不建议在 UI 线程中执行的,所以一般指定 Schedulers.io() 。

BriteDatabase.createQuery方法和SQLiteDatabase.rawQuery方法相比,多了一个table(s)表参数,用于监听数据变更。当我们订阅subscribe返回的Observable<Query>的时候,立刻执行需要的查询语句。

Observable<Query> users = db.createQuery("users", "SELECT * FROM users");
users.subscribe(new Action1<Query>() {
  @Override public void call(Query query) {
    Cursor cursor = query.run();
    // TODO parse data...
  }
});

和传统的rawQuery方法不同的是,只要你订阅了Observable,在你插入更新和删除数据的时候,订阅的查询操作都会得到数据的更新,下面这段代码示范了数据通知的操作。

final AtomicInteger queries = new AtomicInteger();
users.subscribe(new Action1<Query>() {
  @Override public void call(Query query) {
    queries.getAndIncrement();
  }
});
System.out.println("Queries: " + queries.get()); // Prints 1

db.insert("users", createUser("jw", "Jake Wharton"));
db.insert("users", createUser("mattp", "Matt Precious"));
db.insert("users", createUser("strong", "Alec Strong"));

System.out.println("Queries: " + queries.get()); // Prints 4

sqlbrite 使用这个表的名字来通知其他监听该表数据的 Observable 对象来更新数据。这就要求你只能通过 BriteDatabase 来访问数据库, 而不能使用 SQLiteOpenHelper 。
下面代码示范了通过Subscription取消订阅之后,将不会再接收到数据的操作通知。

final AtomicInteger queries = new AtomicInteger();
Subscription s = users.subscribe(new Action1<Query>() {
  @Override public void call(Query query) {
    queries.getAndIncrement();
  }
});
System.out.println("Queries: " + queries.get()); // Prints 1

db.insert("users", createUser("jw", "Jake Wharton"));
db.insert("users", createUser("mattp", "Matt Precious"));
s.unsubscribe();

db.insert("users", createUser("strong", "Alec Strong"));

System.out.println("Queries: " + queries.get()); // Prints 3

如果提交大量数据,则可以使用事务处理:

final AtomicInteger queries = new AtomicInteger();
users.subscribe(new Action1<Query>() {
  @Override public void call(Query query) {
    queries.getAndIncrement();
  }
});
System.out.println("Queries: " + queries.get()); // Prints 1

Transaction transaction = db.newTransaction();
try {
  db.insert("users", createUser("jw", "Jake Wharton"));
  db.insert("users", createUser("mattp", "Matt Precious"));
  db.insert("users", createUser("strong", "Alec Strong"));
  transaction.markSuccessful();
} finally {
  transaction.end();
}

System.out.println("Queries: " + queries.get()); // Prints 2

由于查询只是普通的 RxJava Observable 对象,所以可以在上面使用各种操作函数,这里通过操作符控制通知subscriber的频率:

users.debounce(500, MILLISECONDS).subscribe(new Action1<Query>() {
  @Override public void call(Query query) {
    // TODO...
  }
});

SqlBrite还可以通过封装ContentResolver用于content provider的数据查询操作。

BriteContentResolver resolver = sqlBrite.wrapContentProvider(contentResolver, Schedulers.io());
Observable<Query> query = resolver.createQuery(/*...*/);

当然Rxjava中的combine,filter等操作符也会在使用SqlBrite的时候发挥极大地的作用。


SqlDelight的使用

以下内容来自于官方README.md,由于目前个人还没有使用过SqlDelight,部分不做翻译,请直接查看原文档。将在后续使用后更新

示例

要使用 SQLDelight ,需要把 SQL 语句放到对应的 .sq 文件中,默认目录为和 main 目录下的 java 代码同级,例如
src/main/sqldelight/com/example/HockeyPlayer.sq ,其中 com/example/ 为对应 java 对象的包名字。 在该 .sq 文件中一般第一个语句是创建表的语句:

CREATE TABLE hockey_player (
  _id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
  number INTEGER NOT NULL,
  name TEXT NOT NULL
);

-- 其他的语句通过标识符来引用。在生成的 Java 对象中会包含
-- 一个该标识符的常亮引用这个语句。
select_by_name:
SELECT *
FROM hockey_player
WHERE name = ?;

上面的 SQL 语句会生成一个 HockeyPlayerModel Java 接口。该接口内有两个嵌套类分别把 Cursor 映射为 Java 对象以及把 Java 对象转换为 ContentValues 好插入数据库,这两个嵌套类分别称之为 Mapper 和 Marshal:

package com.example;

import android.content.ContentValues;
import android.database.Cursor;
import java.lang.String;

public interface HockeyPlayerModel {
  String TABLE_NAME = "hockey_player";

  String _ID = "_id";

  String NUMBER = "number";

  String NAME = "name";

  String CREATE_TABLE = ""
      + "CREATE TABLE hockey_player (\n"
      + "  _id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n"
      + "  number INTEGER NOT NULL,\n"
      + "  name TEXT NOT NULL\n"
      + ")";

  String SELECT_BY_NAME = ""
      + "SELECT *\n"
      + "FROM hockey_player\n"
      + "WHERE name = ?";

  long _id();

  long number();

  String name();

  final class Mapper<T extends HockeyPlayerModel> {
    private final Creator<T> creator;

    protected Mapper(Creator<T> creator) {
      this.creator = creator;
    }

    public T map(Cursor cursor) {
      return creator.create(
          cursor.getLong(cursor.getColumnIndex(_ID)),
          cursor.getLong(cursor.getColumnIndex(NUMBER)),
          cursor.getString(cursor.getColumnIndex(NAME))
      );
    }

    public interface Creator<R extends HockeyPlayerModel> {
      R create(long _id, long number, String name);
    }
  }

  class HockeyPlayerMarshal<T extends HockeyPlayerMarshal<T>> {
    protected ContentValues contentValues = new ContentValues();

    public HockeyPlayerMarshal() {
    }

    public final ContentValues asContentValues() {
      return contentValues;
    }

    public T _id(long _id) {
      contentValues.put(_ID, _id);
      return (T) this;
    }

    public T number(long number) {
      contentValues.put(NUMBER, number);
      return (T) this;
    }

    public T name(String name) {
      contentValues.put(NAME, name);
      return (T) this;
    }
  }
}

Read more: [http://blog.chengyunfeng.com/?p=993#ixzz4EqMaUGwO](http://blog.chengyunfeng.com/?p=993#ixzz4EqMaUGwO)

AutoValue

如果你使用过 AutoValue ,则可以发现上面生成的 HockeyPlayerModel 可以直接支持 AutoValue,这样可以大大减少实现这个接口需要编写的代码数量:

@AutoValue
public abstract class HockeyPlayer implements HockeyPlayerModel {
  public static final Mapper<HockeyPlayer> MAPPER = new Mapper<>(new Mapper.Creator() {
    @Override public HockeyPlayer create(long _id, long number, String name) {
      return new AutoValue_HockeyPlayer(_id, age, number, gender);
    }
  }

  public static final class Marshal extends HockeyPlayerMarshal<Marshal> { }
}

配合 Retrolambda 则代码可以进一步简化:

@AutoValue
public abstract class HockeyPlayer implements HockeyPlayerModel {
  public static final Mapper<HockeyPlayer> MAPPER = new Mapper<>(AutoValue_HockeyPlayer::new);

  public static final class Marshal extends HockeyPlayerMarshal<Marshal> { }
}

使用 AutoValue 自动生成对应的 Java 模型,然后用 AutoValue 生成的 Java 模型实现类来创建一个 静态的 MAPPER 。这样读取数据可以使用 MAPPER, 插入数据可以使用 Marshal:

public void insert(SqliteDatabase db, long _id, long number, String name) {
  db.insert(HockeyPlayer.TABLE_NAME, null, new HockeyPlayer.Marshal()
    ._id(_id)
    .number(number)
    .name(name)
    .asContentValues());
}

public List<HockeyPlayer> alecs(SQLiteDatabase db) {
  List<HockeyPlayer> result = new ArrayList<>();
  try (Cursor cursor = db.rawQuery(HockeyPlayer.SELECT_BY_NAME, new String[] { "Alec" })) {
    while (cursor.moveToNext()) {
      result.add(HockeyPlayer.MAPPER.map(cursor));
    }
  }
  return result;
}

数据类型

SQLDelight 的数据列定义和普通的 SQLite 列定义一样,但是额外支持一些指定 Java 数据类型的自定义属性。SQLDelight 直接支持 Cursor 和 ContentValues 需要的数据类型:

CREATE TABLE some_types {
  some_long INTEGER,           -- Stored as INTEGER in db, retrieved as Long
  some_double REAL,            -- Stored as REAL in db, retrieved as Double
  some_string TEXT,            -- Stored as TEXT in db, retrieved as String
  some_blob BLOB,              -- Stored as BLOB in db, retrieved as byte[]
  some_int INTEGER AS Integer, -- Stored as INTEGER in db, retrieved as Integer
  some_short INTEGER AS Short, -- Stored as INTEGER in db, retrieved as Short
  some_float REAL AS Float     -- Stored as REAL in db, retrieved as Float
}

Booleans

SQLDelight 使用和 SQLite 一样的类型来支持布尔值。但是在编写 SQL 语句的时候,可以通过额外的属性来指定该字段为布尔值,这样生成的 Java 对象的类型为 布尔值:

CREATE TABLE hockey_player (
  injured INTEGER AS Boolean DEFAULT 0
)

自定义类型

如果上面的基本类型不满足要求还可以自定义类型,直接指定对应的 Java 类型:

import java.util.Calendar;
CREATE TABLE hockey_player ( 
birth_date INTEGER AS Calendar NOT NULL
)

如果使用了自定义类型,则需要同时创建一个 ColumnAdapter 来把 Cursor 的数据转换为对应的类型;同时把 Java 类型数据转换为 ContentValues 支持的数据:

public class HockeyPlayer implements HockeyPlayerModel {
  private static final ColumnAdapter<Calendar> CALENDAR_ADAPTER = new ColumnAdapter<>() {
    @Override public Calendar map(Cursor cursor, int columnIndex) {
      Calendar calendar = Calendar.getInstance();
      calendar.setTimeInMillis(cursor.getLong(columnIndex));
      return calendar;
    }

    @Override public void marshal(ContentValues contentValues, String key, Calendar value) {
      contentValues.put(key, value.getTimeInMillis());
    }
  }

  public static final Mapper<HockeyPlayer> MAPPER = new Mapper<>(new Mapper.Creator<>() { },
    CALENDAR_ADAPTER);

  public static final class Marshal extends HockeyPlayerMarshal<Marshal> {
    public Marshal() {
      super(CALENDAR_ADAPTER);
    }
  }
}

枚举类型

为了方便支持枚举类型,SQLDelight 包含了一个 ColumnAdapter把枚举类型保存为 TEXT:

import com.example.hockey.HockeyPlayer;

CREATE TABLE hockey_player (
  position TEXT AS HockeyPlayer.Position
)
public class HockeyPlayer implements HockeyPlayerModel {
  public enum Position {
    CENTER, LEFT_WING, RIGHT_WING, DEFENSE, GOALIE
  }

  private static final ColumnAdapter<Position> POSITION_ADAPTER = EnumColumnAdapter.create(Position.class);

  public static final Factory<HockeyPlayer> FACTORY = new Factory<>(new Creator<>() { },
      POSITION_ADAPTER);
}

Projection

请直接查看原文档README.md

Views

请直接查看原文档README.md

Join 操作

请直接查看原文档README.md

SQL 语句参数

.sq 文件中的语句也可以
支持SqliteDatabase中的参数:

select_by_position:
SELECT *
FROM hockey_player
WHERE position = ?;
Cursor centers = db.rawQuery(HockeyPlayer.SELECT_BY_POSITION, new String[] { Center.name() });

Intellij(Android Studio) 插件

插件用来帮助编辑 .sq 文本,支持如下功能:

  • 语法高亮
  • 重构/查找 标识符
  • 代码自动补全
  • 编辑后自动生成 Model 接口

安装使用

Gradle 插件:

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'com.squareup.sqldelight:gradle-plugin:0.4.3'
  }
}
apply plugin: 'com.squareup.sqldelight'

关于SqlBrite和SqlDelight的思考

有兴趣的可以看看大神Jack Wharton在国外著名新闻站点Reddit中的回答。这里做出翻译。

SqlBrite和SqlDelight都是对象映射(OM,Object Mappers)而不是对象关系映射(ORM,Object/Relational Mappers)。

ORM 其实并不是一个优秀的框架。很多平台的 ORM 实现都有性能和内存的问题。我们也不会编写ORM。

SqlBrite只是让你方便在 RxJava 中使用 Sql 操作而已,并且额外添加了对数据库表数据更新通知的机制。只是一个 SQLiteOpenHelper 的轻量级封装,并不关心你的对象是如何实现的,也不关心你的数据库。同样,SqlBrite也不支持对象映射和类型安全的查询,通常这些功能并不比直接使用SQL 语句更加方便。虽然在 Java 中操作 SQL 语言有一个比较好的框架 — jOOQ 。但是在 Android 中使用 jOOQ 就是杀鸡用牛刀了!

SqlDelight 的做法是从 SQL 语句来生成 JAVA 模型代码。 这样的好处是,所有 SQL 语句都位于同一个位置,通过查看 SQL 语句可以清楚的了解需要实现的功能和数据库的结构。SqlDelight 添加了对 SQL 语句的编译时验证、表名字和列名字的代码自动完成功能。让编写 SQL 语句更加快捷。在编译的时候,根据 SQL 语句生成 Java 模型接口和 builder 来把数据行和 Java 对象实现转换。虽然这个框架还很年轻,但是通过这个框架目前的功能你就可以发现,SqlDelight 不会变成一个 ORM 框架。并且不会做很重的功能(比如数据懒加载、缓存 、级联删除 等 ORM 框架内常见的功能) 。

SqlDelight 大部分代码都是编译时用的,真正的运行时代码(包含在你应用中的代码)只有10几行代码几个接口而已。它将会使你的SQL编写更加简单,迁移到上面这两个库也会非常的简单,同时你也能享受到响应式的查询,类型安全的对象映射和编译的优点。

这两个框架将不会实现那些ORM框架强制要求你做的事情下面这些功能:

  • 不会成为 Java 语言中功能不够全面的数据库查询 API
  • 不会实现把外键映射为 Java 对象集合(关系映射)
  • 不会有泛字符类型(string-ly typed)的表名字和列名字的引用
  • 不会有一个基类需要你的数据库操作对象来继承该类
  • 不会在 Java 中定义数据库表,比如通过注解、或者继承一个类等
  • 不会自动创建数据表和迁移数据表
  • 不会对 Sql 查询和 Java 对象做线程限制
  • 不会返回可变的对象,你修改该对象的值,然后调用 save 函数就可以把更新的值保存到数据库了。

SqlBrite 仅仅是一个用来协调更新数据和通知数据变化的轻量级封装,当你对数据表进行操作的时候,其他订阅者可以在数据发生变化的时候收到通知。然后可以用 RxJava 的方式来操作数据。
SqlBrite 不是一个 ORM 框架,也不是一个类型安全的查询框架。不会提供类似Gson中对象序列化的功能,也不会提供数据库迁移的功能。其中的一些功能由可以与SqlBrite一起使用的 SQLDelight提供。


参考链接

为什么要用 SQLDelight 和 SQLBrite
SQLBrite: A Reactive Database Foundation
SQLite, ORMs, NoSQL: what Android developers use and why
Anyone use SQLBrite and/or SQLDelight?
完美的安卓 model 层架构(上)