StorIO — modern API for SQLiteDatabase and ContentResolver
- Powerful & Simple set of Operations:
Put
,Get
,Delete
- Convenient builders with compile-time guarantees. Forget about 6-7
null
in queries - No reflection, no annotations,
StorIO
is not ORM - Every Operation over
StorIO
can be executed as blocking call or asrx.Observable
-
RxJava
as first class citizen, but it's not required dependency! -
Observable
fromGet
Operation can observe changes inStorIO
and receive updates automatically - If you don't want to work with
Cursor
andContentValue
you don't have to -
StorIO
is replacements forSQLiteDatabase
andContentResolver
-
StorIO
+RxJava
is replacement forLoaders
- We are working on
MockStorIO
for easy unit testing
0. Create an instance of StorIOSQLite
StorIOSQLite storIOSQLite = new DefaultStorIOSQLite.Builder() .sqliteOpenHelper(yourSqliteOpenHelper) // or .db(db) .build();
It's a good practice to use one instance of StorIOSQLite
per database.
1. Get Operation
Get list of objects with blocking call:
// it's a good practice to store MapFunc as public static final field somewhere final MapFunc mapFunc = new MapFunc() { @Override public Tweet map(Cursor cursor) { // no need to move cursor and close it, StorIO will handle it for you return new Tweet(); // fill with values from cursor } }; final List tweets = storIOSQLite .get() .listOfObjects(Tweet.class) .withQuery(new Query.Builder() .table("tweets") .build()) .withMapFunc(mapFunc) .prepare() .executeAsBlocking();
Get Cursor
via blocking call:
final Cursor tweetsCursor = storIOSQLite .get() .cursor() .withQuery(new Query.Builder() .table("tweets") .build()) .prepare() .executeAsBlocking();
Things become much more interesting with RxJava
!
Get cursor as Observable
storIOSQLite .get() .cursor() .withQuery(new Query.Builder() .table("tweets") .build()) .prepare() .createObservable() .subscribeOn(Schedulers.io()) // moving Get Operation to other thread .observeOn(AndroidSchedulers.mainThread()) // observing result on Main thread .subscribe(new Action1() { @Override public void call(Cursor cursor) { // display the data from cursor // will be called once } });
What if you want to observe changes in StorIOSQLite
?
First-case: Receive updates to Observable
on each change in tables from Query
storIOSQLite .get() .listOfObjects(Tweet.class) .withQuery(Tweet.ALL_TWEETS_QUERY) .withMapFunc(Tweet.MAP_FROM_CURSOR) .prepare() .createObservableStream() // here is the magic! It will be subscribed to changes in tables from Query .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1>() { @Override public void call(List tweets) { // display the data // magic: this will be called on each update in "tweets" table } }); // don't forget to manage Subscription and unsubscribe in lifecycle methods to prevent memory leaks
Second case: Handle changes manually
storIOSQLite .observeChangesInTable("tweets") .subscribe(new Action1() { // or apply RxJava Operators // do what you want! });
Get result with RawQuery with joins and other SQL things
storIOSQLite .get() .listOfObjects(TweetAndUser.class) .withQuery(new RawQuery.Builder() .query("SELECT * FROM tweets JOIN users ON tweets.user_name = users.name WHERE tweets.user_name = ?") .args("artem_zin") .build()) .withMapFunc(TweetAndUser.MAP_FROM_CURSOR) .prepare() .createObservableStream();
Customize behavior of Get
Operation with GetResolver
GetResolver getResolver = new GetResolver() { // Performs Get for RawQuery @Override @NonNull public Cursor performGet(@NonNull StorIOSQLite storIOSQLite, @NonNull RawQuery rawQuery) { Cursor cursor = ...; // get result as you want, or add some additional behavior return cursor; } // Performs Get for Query @Override @NonNull public Cursor performGet(@NonNull StorIOSQLite storIOSQLite, @NonNull Query query) { Cursor cursor = ...; // get result as you want, or add some additional behavior return cursor; } }; storIOSQLite .get() .listOfObjects(Tweet.class) .withQuery(Tweet.ALL_TWEETS_QUERY) .withMapFunc(Tweet.MAP_FROM_CURSOR) .withGetResolver(getResolver) // here we set custom GetResolver for Get Operation .prepare() .executeAsBlocking();
Several things about Get
Operation:
- There is
DefaultGetResolver
which simply redirects query toStorIOSQLite
,Get
Operation will useDefaultGetResolver
if you won't pass yourGetResolver
, in 99% of casesDefaultGetResolver
will be enough - As you can see, results of
Get
Operation computed even if you'll applyRxJava
operators such asDebounce
, if you want to avoid unneeded computations, please combineStorIOSQLite.observeChangesInTable()
withGet
Operation manually. - In
StorIO 1.1.0
we are going to addLazy
to allow you skip unneeded computations - If you want to
Put
multiple items intoStorIOSQLite
, better to do this in transaction to avoid multiple calls to the listeners (see docs aboutPut
Operation)
2. Put Operation
Put
Operation requires PutResolver
which defines the behavior of Put
Operation (insert or update).
You have two ways of implementing PutResolver
:
1) Easy: extend DefaultPutResolver
and implement it correctly
DefaultPutResolver
will search for field _id
in ContentValues
and will perform insert if there is no value or update if there _id
is not null.
2) Implement PutResolver
and perform put as you need.
In 99% of cases your tables have _id
column as unique id and DefaultPutResolver
will be enough
public static final PutResolver PUT_RESOLVER = new DefaultPutResolver<>() { @Override @NonNull protected String getTable() { return "tweets"; } @Override public void afterPut(@NonNull Tweet tweet, @NonNull PutResult putResult) { // callback were you can change object after insert if (putResult.wasInserted()) { tweet.setId(putResult.getInsertedId()); } } };
Put object of some type
Tweet tweet = getSomeTweet(); storIOSQLite .put() .object(tweet) .withPutResolver(Tweet.PUT_RESOLVER) .withMapFunc(Tweet.MAP_TO_CONTENT_VALUES) .prepare() .executeAsBlocking(); // or createObservable()
Put multiple objects of some type
List tweets = getSomeTweets(); storIOSQLite .put() .objects(tweets) .withPutResolver(Tweet.PUT_RESOLVER) .withMapFunc(Tweet.MAP_TO_CONTENT_VALUES) .prepare() .executeAsBlocking(); // or createObservable()
Put ContentValues
ContentValues contentValues = getSomeContentValues(); storIOSQLite .put() .contentValues(contentValues) .withPutResolver(putResolver) .prepare() .executeAsBlocking(); // or createObservable()
Several things about Put
Operation:
-
Put
Operation requiresPutResolver
,StorIO
requires it to avoid reflection -
Put
Operation can be executed in transaction and by default it will use transaction, you can customize this viauseTransactionIfPossible()
ordontUseTransaction()
-
Put
Operation in transaction will produce only one notification toStorIOSQLite
observers - Result of
Put
Operation can be useful if you want to know what happened: insert (and insertedId) or update (and number of updated rows)
3. Delete Operation
Delete object
// you can store it as static final field somewhere final MapFunc mapToDeleteQuery = new MapFunc() { @Override public DeleteQuery map(Tweet tweet) { return new DeleteQuery.Builder() .table(Tweet.TABLE) .where(Tweet.COLUMN_ID) .whereArgs(String.valueOf(tweet.getId())) .build(); } }; Tweet tweet = getSomeTweet(); storIOSQLite .delete() .object(tweet) .withMapFunc(mapToDeleteQuery) .prepare() .executeAsBlocking(); // or createObservable()
Delete multiple objects
List tweets = getSomeTweets(); storIOSQLite .delete() .objects(tweets) .withMapFunc(mapToDeleteQuery) .prepare() .executeAsBlocking(); // or createObservable()
Several things about Delete
Operation:
-
Delete
Operation of multiple items can be performed in transaction, by default it will use transaction if possible - Same rules as for
Put
Operation about notifications forStorIOSQLite
observers: transaction -> one notification, without transaction - multiple notifications - Result of
Delete
Operation can be useful if you want to know what happened
4. ExecSql Operation
Sometimes you need to execute raw sql, StorIOSQLite
allows you to do it
storIOSQLite .execSql() .withQuery(new RawQuery.Builder() .query("ALTER TABLE ? ADD COLUMN ? INTEGER") .args("tweets", "number_of_retweets") .affectedTables("tweets") // optional: you can specify affected tables to notify Observers .build()) .prepare() .executeAsBlocking(); // or createObservable()
Several things about ExecSql
:
- Use it for non insert/update/query/delete operations
- Notice that you can set list of tables that will be affected by
RawQuery
andStorIOSQLite
will notify tables Observers
For more examples, please check our Design Tests
:
Architecture:
StorIOSQLite
and StorIOContentResolver
— are abstractions with default implementations: DefaultStorIOSQLite
and DefaultStorIOContentResolver
.
It means, that you can have your own implementation of StorIOSQLite
and StorIOContentResolver
with custom behavior, such as memory caching, verbose logging and so on.
One of the main goals of StorIO
— clean API which will be easy to use and understand, that's why StorIOSQLite
and StorIOContentResolver
have just several methods, but we understand that sometimes you need to go under the hood and StorIO
allows you to do it: StorIOSQLite.Internal
and StorIOContentResolver.Internal
encapsulates low-level methods, you can use them if you need, but please try to avoid it.
Queries
All Query
objects are immutable, you can share them safely.
Concept of Prepared Operations
You may notice that each Operation (Get, Put, Delete) should be prepared with prepare()
. StorIO
has an entity called PreparedOperation
, and you can use them to perform group execution of several Prepared Operations or provide PreparedOperation
as a return type of your API (for example in Model layer) and client will decide how to execute it: executeAsBlocking()
or createObservable()
or createObservableStream()
(if possible). Also, Prepared Operations might be useful for ORMs based on StorIO
.
You can customize behavior of every Operation via Resolvers
: GetResolver
, PutResolver
, DeleteResolver
.
Made with love in Pushtorefresh.com by @artem_zin and @nikitin-da