引言
之前看北京GDG直播收获颇丰,我打算用Github API来实践一下Piasy提出的完美model,这是这个系列的第二篇,时下非常流行apt生成代码,大家喜闻乐见的ButterKnife就是一个典型的例子,Google出品的AutoValue也是其中的翘楚,这篇文章讲的是怎么使用Sqldelight配合AutoValue完成数据的持久化(数据库),以及使用SqlBrite访问数据库,希望能通过这篇文章让你了解Sqldelight和SqlBrite。
没看第一篇的同学先补补课:
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文件书写
首先要有一个正确的路径,我的路径是这样的,注意包名要一致,包的位置不要搞错
在.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_BigmercuTest
和AutoValue_BigmercuTest
$AutoValue_BigmercuTest
就是标准的AutoValue声明实体的方式AutoValue_BigmercuTest
就是最后生成的实体
结构图,将就看一下,结构比较简单就不画类图了
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(); } |
共同学习,写下你的评论
评论加载中...
作者其他优质文章