完美 Model 层之 sqldelight 使用

2,057 阅读7分钟

引言

之前看北京GDG直播收获颇丰,我打算用Github API来实践一下Piasy提出的完美model,这是这个系列的第二篇,时下非常流行apt生成代码,大家喜闻乐见的ButterKnife就是一个典型的例子,Google出品的AutoValue也是其中的翘楚,这篇文章讲的是怎么使用Sqldelight配合AutoValue完成数据的持久化(数据库),以及使用SqlBrite访问数据库,希望能通过这篇文章让你了解Sqldelight和SqlBrite。
没看第一篇的同学先补补课:

完美Model之AutoValue使用

github

在这个小项目中我将实践这个系列文章的所有技术,正在持续更新。
github.com/Bigmercu/Pe…
欢迎star fork issue

Sqldelight 建造数据库和实体

之前文章我有说过一种观点,为啥有的工具老觉得使用起来很麻烦,为什么原作不再封装一下,那是因为封装度越高,可DIY性越少。
现在流行的数据库访问相关的开源库不少,比如GreenDao,Realm等等,大多都是ORM(Object Relational Mapping)架构的,但是,ORM可能存在性能和内存的问题以及难以实现复杂的功能,ORM使用起来确实方便快捷,但是封装度比较高,所以可DIY性比较低,square公司这个业界良心不能忍受,所以开源了Sqldelight

  • 它可以让我们根据SQL代码直接生成实体
  • 轻量级,可以真正,大部分代码都是运行时生成
  • 和AutoValue结合以后大大减少了代码量
  • 和Rxjava结合使用

生成实体和数据库Model

Gradle配置

在Project#build.gradle中

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

在Model#build.gradle中加入

apply plugin: 'com.squareup.sqldelight'
dependencies {
 ...
   compile 'com.squareup.sqlbrite:sqlbrite:0.7.0'

.sq文件书写

首先要有一个正确的路径,我的路径是这样的,注意包名要一致,包的位置不要搞错

在.sq文件里要写什么?

CREATE TABLE bigmercu_test (
 id INTEGER NOT NULL PRIMARY KEY,
 login TEXT NOT NULL COLLATE NOCASE,
 age INTEGER,
 email TEXT,
 name TEXT
 select_by_name:
 SELECT *
 FROM bigmercu_test
 WHERE name = ?;

首先是一个建表语句,它支持这些数据类型:

some_long INTEGER,           
some_double REAL,            
some_string TEXT,            
some_blob BLOB,              
some_int INTEGER AS Integer, 
some_short INTEGER AS Short, 
some_float REAL AS Float     

然后就是一个查询语句,和一般查询语句没有区别,在前面有一个select_by_login:这里是你给这个查询定义的一个Name,使用的时候需要通过它来调用。

写好.sq文件以后,rebuild一下,生成了如下的Model接口

public interface BigmecuTestModel {
 String TABLE_NAME = "bigmercu_test";
 String ID = "id";
 String LOGIN = "login";
 String AGE = "age";
 String EMAIL = "email";
 String NAME = "name";
 String CREATE_TABLE = ""
     + "CREATE TABLE bigmercu_test (\n"
     + "  id INTEGER NOT NULL PRIMARY KEY,\n"
     + "  login TEXT NOT NULL COLLATE NOCASE,\n"
     + "  age INTEGER,\n"
     + "  email TEXT,\n"
     + "  name TEXT\n"
     + ")";
 String SELECT_BY_NAME = ""
     + "SELECT *\n"
     + "FROM bigmercu_test\n"
     + "WHERE name = ?";
 long id();
 String login();
 Long age();
 String email();
 String name();
 interface Creator {
   T create(long id, @NonNull String login, @Nullable Long age, @Nullable String email, @Nullable String name);
 final class Mapper implements RowMapper {
   private final Factory bigmecuTestModelFactory;
   public Mapper(Factory bigmecuTestModelFactory) {
     this.bigmecuTestModelFactory = bigmecuTestModelFactory;
   public T map(@NonNull Cursor cursor) {
     return bigmecuTestModelFactory.creator.create(
         cursor.getLong(0),
         cursor.getString(1),
         cursor.isNull(2) ? null : cursor.getLong(2),
         cursor.isNull(3) ? null : cursor.getString(3),
         cursor.isNull(4) ? null : cursor.getString(4)
 final class Marshal {
   protected final ContentValues contentValues = new ContentValues();
   Marshal( BigmecuTestModel copy) {
     if (copy != null) {
       this.id(copy.id());
       this.login(copy.login());
       this.age(copy.age());
       this.email(copy.email());
       this.name(copy.name());
   public ContentValues asContentValues() {
     return contentValues;
   public Marshal id(long id) {
     contentValues.put(ID, id);
     return this;
   public Marshal login(String login) {
     contentValues.put(LOGIN, login);
     return this;
   public Marshal age(Long age) {
     contentValues.put(AGE, age);
     return this;
   public Marshal email(String email) {
     contentValues.put(EMAIL, email);
     return this;
   public Marshal name(String name) {
     contentValues.put(NAME, name);
     return this;
 final class Factory {
   public final Creator creator;
   public Factory(Creator creator) {
     this.creator = creator;
   public Marshal marshal() {
     return new Marshal(null);
   public Marshal marshal(BigmecuTestModel copy) {
     return new Marshal(copy);
   public Mapper select_by_nameMapper() {
     return new Mapper(this);

可以看到,里面包含了

  • 表名
  • 建表语句->CREATE_TABLE
  • 前面在.sq里面写的query语句->SELECT_BY_NAME
  • long id();还有类似这样的声明,看过我上一篇的同学应该熟悉,这是给AutoValue用于生成Entry的
  • Creator用于创建一个对象,在这里就是创建AutoValue_BigmecuTestModel对象
  • Mapper用于映射,(将query结果存到Cursor缓冲区,使用Creator方法)返回一个(AutoValue_BigmecuTestModel)对象。
  • Marshal(马歇尔???)用于通过数据(一个对象)来创建一个ContentValues,sqlDelite的update方法参数如下,其中的ContentValues就是由Marshal()来创建

    public int update(@NonNull String table, @NonNull ContentValues values,
        @Nullable String whereClause, @Nullable String... whereArgs) {
      return update(table, values, CONFLICT_NONE, whereClause, whereArgs);
  • Factory方法:可以看到里面包括了Creator,Marshal还有我们自己定义的select_by_nameMapper,对,他就是一个仓库,通过它我们可以访问除了Mapper以外的方法。

实体

上面生成了BigmecuTestModel接口,通过实现这个Model接口我们就可以生成实体了。
这里是AutoValue拓展了一下,没看AutoValue的同学在这里补课:完美Model之AutoValue使用

  • 类名保持和.sq文件的一致,就是后续实体的名字。
  • 将类声明为abstract并实现前面生成的Model接口。
  • 声明一个FACTORY静态对象,在其中实现create方法,并返回实体的对象。后续我们就通过这个FACTORY去访问大部分Sqldelight提供的方法(除了MAPPER)。
  • typeAdapter 是用于Gson的适配器。
  • MAPPER 前面说了,我们通过查询得到cursor,在MAPPER.map(cursor)中传入cursor就能返回一个对象。
public abstract class BigmercuTest implements BigmecuTestModel {
   public static final BigmercuTest.Factory FACTORY = new Factory<>(new Creator() {
       public BigmercuTest create(long id, @NonNull String login, @Nullable Long age,
                                      @Nullable String email, @Nullable String name) {
           return new AutoValue_BigmercuTest(id,login,age,email,name);
   });
   public static TypeAdapter typeAdapter(final Gson gson){
       return new AutoValue_BigmercuTest.GsonTypeAdapter(gson);
   public static final RowMapper MAPPER = FACTORY.select_by_nameMapper();

这个实体生成了$AutoValue_BigmercuTestAutoValue_BigmercuTest
$AutoValue_BigmercuTest就是标准的AutoValue声明实体的方式
AutoValue_BigmercuTest就是最后生成的实体
结构图,将就看一下,结构比较简单就不画类图了

SqlBrite 访问数据库

SQLiteOpenHelper

使用原始的方法访问数数据库肯定是需要SQLiteOpenHelper的:
一个典型的SQLiteOpenHelper,加入了单利模式获取helper实例。

public final class GithubUserHepler extends SQLiteOpenHelper {
   private static final String TAG = GithubUserHepler.class.getSimpleName();
   private static final String DB_NAME = "GithubUserDB"; 
   private static final int DATABASE_VERSION = 1;
   private static GithubUserHepler mGithubUserHepler;
   public GithubUserHepler(Context context) {
       super(context, DB_NAME, null, DATABASE_VERSION);
   public static GithubUserHepler gitInstance(){
       return GithubUserHeplerInstanceHolder.hepler;
   public static class GithubUserHeplerInstanceHolder{
       private static GithubUserHepler hepler = new GithubUserHepler(ContextHolder.getContext());
   public void onCreate(SQLiteDatabase sqLiteDatabase) {
       sqLiteDatabase.execSQL(GithubUser.CREATE_TABLE);
   public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
       Log.d(TAG,"upgrade");

这样就完成了第一步,建好了数据库,有了实体,还定义了一些访问操作。

访问操作

初始化操作

这样就能获得一个操作数据库读写的对象 mBriteDatabase

SqlBrite sqlBrite = SqlBrite.create();
mBriteDatabase = sqlBrite.wrapDatabaseHelper(GithubUserHepler.gitInstance(),Schedulers.io());

这里使用GithubUser.FACTORY.marshal()来生成contentValues,根据id进行插入,需要注意的是,这里id要写成id=?,因为在标准SQL语句中最后是where id = ?这里用问号来等待后续参数填入。

mBriteDatabase.update(GithubUser.TABLE_NAME,GithubUser.FACTORY.marshal()
                                .avatar_url(githubUser.avatar_url())
                              ...
                                .repos_url(githubUser.repos_url())
                                .asContentValues(),"id=?", String.valueOf(githubUser.id()));

insert操作同理

mBriteDatabase.insert(GithubUser.TABLE_NAME,GithubUser.FACTORY.marshal()
                                 .avatar_url(githubUser.avatar_url())
                               ...
                                 .repos_url(githubUser.repos_url())
                                 .asContentValues(), SQLiteDatabase.CONFLICT_IGNORE);

update操作不仅仅插入数据,源码文档里这么说

Update rows in the specified {@code table} and notify any subscribed queries. This method
will not trigger a notification if no rows were updated.

insert的文档这么说

Insert a row into the specified {@code table} and notify any subscribed queries.

它们不仅仅会更新数据,同事还会通知所有的订阅者,之前说过,它的很强的一个使用就是可以和Rxjava结合使用,当我们在Rxjava中执行一个查询以后,后续数据更新会自动通知查询订阅者。这才是它牛逼之处之一。

和Rxjava结合

使用方法和Retrofit类似,GithubUser.SELECT_BY_LOGIN是我们在前面的.sq文件中定义的查询,输入一个参数,参数得用new String[]{name}的形式,然后就能得到一个SqlBrite.Query,run这个query可以得到一个Cursor,通过这个Cursor就可以得到对象了,神奇之处在于,当你查询的数据源,比如我查询id=007的User,我在其他地方update这个user,这里会被通知User更新,Observable会发射最新的User。

我在这里面的使用场景是,我先从本地获取数据显示,再从网络获取数据存入数据库且不用通知更新,已经订阅的数据库查询Observable会收到notify然后发射最新的数据,简单的解决了两个数据源的问题。

mLocalDataSubscription = mBriteDatabase
             .createQuery(GithubUser.TABLE_NAME, GithubUser.SELECT_BY_LOGIN, new String[]{name})
             .observeOn(AndroidSchedulers.mainThread())
             .subscribe(new Action1() {
                 public void call(SqlBrite.Query query) {
                     Cursor cursor = query.run();
                     while (cursor.moveToNext()) {
                         mGithubUser = GithubUser.MAPPER.map(cursor);
                     if(mGithubUser != null){
                         Log.d(TAG,"local data:  " + mGithubUser.toString());
                         listener.onSuccess(mGithubUser);
             });

最后,不要忘了关闭数据库和取消订阅,避免内存泄露。

mBriteDatabase.close();
if(mLocalDataSubscription != null && !mLocalDataSubscription.isUnsubscribed()){
   mLocalDataSubscription.unsubscribe();

参考文献

完美的安卓 model 层架构(上)
sqlbrite官方文档
sqldelight官方文档