继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

完美Model层之sqldelight使用

开满天机
关注TA
已关注
手记 94
粉丝 74
获赞 262

引言

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

完美Model之AutoValue使用

github

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

Sqldelight 建造数据库和实体

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

o    它可以让我们根据SQL代码直接生成实体

o    轻量级,可以真正,大部分代码都是运行时生成

o    和AutoValue结合以后大大减少了代码量

o    和Rxjava结合使用

生成实体和数据库Model

Gradle配置

在Project#build.gradle中

1
2
3
4
5
6
7
8
 buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'com.squareup.sqldelight:gradle-plugin:0.4.4'
  }
}

在Model#build.gradle中加入

1
2
3
4
5
apply plugin: 'com.squareup.sqldelight'
dependencies {
  ...
    compile 'com.squareup.sqlbrite:sqlbrite:0.7.0'
}

.sq文件书写

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

在.sq文件里要写什么?

1
2
3
4
5
6
7
8
9
10
11
12
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 = ?;

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

1
2
3
4
5
6
7
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

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

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

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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();
  @NonNull
  String login();
  @Nullable
  Long age();
  @Nullable
  String email();
  @Nullable
  String name();
  interface Creator<T extends BigmecuTestModel> {
    T create(long id, @NonNull String login, @Nullable Long age, @Nullable String email, @Nullable String name);
  }
  final class Mapper<T extends BigmecuTestModel> implements RowMapper<T> {
    private final Factory<T> bigmecuTestModelFactory;
    public Mapper(Factory<T> bigmecuTestModelFactory) {
      this.bigmecuTestModelFactory = bigmecuTestModelFactory;
    }
    @Override
    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(@Nullable 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<T extends BigmecuTestModel> {
    public final Creator<T> creator;
    public Factory(Creator<T> creator) {
      this.creator = creator;
    }
    public Marshal marshal() {
      return new Marshal(null);
    }
    public Marshal marshal(BigmecuTestModel copy) {
      return new Marshal(copy);
    }
    public Mapper<T> select_by_nameMapper() {
      return new Mapper<T>(this);
    }
  }
}

可以看到,里面包含了

o    表名

o    建表语句->CREATE_TABLE

o    前面在.sq里面写的query语句->SELECT_BY_NAME

o    long id();还有类似这样的声明,看过我上一篇的同学应该熟悉,这是给AutoValue用于生成Entry的

o    Creator用于创建一个对象,在这里就是创建AutoValue_BigmecuTestModel对象

o    Mapper用于映射,(将query结果存到Cursor缓冲区,使用Creator方法)返回一个(AutoValue_BigmecuTestModel)对象。

o    Marshal(马歇尔???)用于通过数据(一个对象)来创建一个ContentValues,sqlDelite的update方法参数如下,其中的ContentValues就是由Marshal()来创建

1
2
3
4
public int update(@NonNull String table, @NonNull ContentValues values,
     @Nullable String whereClause, @Nullable String... whereArgs) {
   return update(table, values, CONFLICT_NONE, whereClause, whereArgs);
 }

o    Factory方法:可以看到里面包括了Creator,Marshal还有我们自己定义的select_by_nameMapper,对,他就是一个仓库,通过它我们可以访问除了Mapper以外的方法。

实体

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

o    类名保持和.sq文件的一致,就是后续实体的名字。

o    将类声明为abstract并实现前面生成的Model接口。

o    声明一个FACTORY静态对象,在其中实现create方法,并返回实体的对象。后续我们就通过这个FACTORY去访问大部分Sqldelight提供的方法(除了MAPPER)。

o    typeAdapter 是用于Gson的适配器。

o    MAPPER 前面说了,我们通过查询得到cursor,在MAPPER.map(cursor)中传入cursor就能返回一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@AutoValue
public abstract class BigmercuTest implements BigmecuTestModel {
    public static final BigmercuTest.Factory<BigmercuTest> FACTORY = new Factory<>(new Creator<BigmercuTest>() {
        @Override
        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<BigmercuTest> typeAdapter(final Gson gson){
        return new AutoValue_BigmercuTest.GsonTypeAdapter(gson);
    }
    public static final RowMapper<BigmercuTest> MAPPER = FACTORY.select_by_nameMapper();
}

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

SqlBrite 访问数据库

SQLiteOpenHelper

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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());
    }
    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL(GithubUser.CREATE_TABLE);//初始化表
    }
    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        Log.d(TAG,"upgrade");
    }
}

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

访问操作

初始化操作

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

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

update && insert

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

1
2
3
4
5
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操作同理

1
2
3
4
5
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然后发射最新的数据,简单的解决了两个数据源的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mLocalDataSubscription = mBriteDatabase
              .createQuery(GithubUser.TABLE_NAME, GithubUser.SELECT_BY_LOGIN, new String[]{name})
              .observeOn(AndroidSchedulers.mainThread())
              .subscribe(new Action1<SqlBrite.Query>() {
                  @Override
                  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);
                      }
                  }
              });

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

1
2
3
4
mBriteDatabase.close();
if(mLocalDataSubscription != null && !mLocalDataSubscription.isUnsubscribed()){
    mLocalDataSubscription.unsubscribe();
}

原文链接:http://www.apkbus.com/blog-705730-62441.html

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP