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
nullin queries - No reflection, no annotations,
StorIOis not ORM - Every Operation over
StorIOcan be executed as blocking call or asrx.Observable -
RxJavaas first class citizen, but it's not required dependency! -
ObservablefromGetOperation can observe changes inStorIOand receive updates automatically - If you don't want to work with
CursorandContentValueyou don't have to -
StorIOis replacements forSQLiteDatabaseandContentResolver -
StorIO+RxJavais replacement forLoaders - We are working on
MockStorIOfor 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 leaksSecond 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
DefaultGetResolverwhich simply redirects query toStorIOSQLite,GetOperation will useDefaultGetResolverif you won't pass yourGetResolver, in 99% of casesDefaultGetResolverwill be enough - As you can see, results of
GetOperation computed even if you'll applyRxJavaoperators such asDebounce, if you want to avoid unneeded computations, please combineStorIOSQLite.observeChangesInTable()withGetOperation manually. - In
StorIO 1.1.0we are going to addLazyto allow you skip unneeded computations - If you want to
Putmultiple items intoStorIOSQLite, better to do this in transaction to avoid multiple calls to the listeners (see docs aboutPutOperation)
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:
-
PutOperation requiresPutResolver,StorIOrequires it to avoid reflection -
PutOperation can be executed in transaction and by default it will use transaction, you can customize this viauseTransactionIfPossible()ordontUseTransaction() -
PutOperation in transaction will produce only one notification toStorIOSQLiteobservers - Result of
PutOperation 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:
-
DeleteOperation of multiple items can be performed in transaction, by default it will use transaction if possible - Same rules as for
PutOperation about notifications forStorIOSQLiteobservers: transaction -> one notification, without transaction - multiple notifications - Result of
DeleteOperation 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
RawQueryandStorIOSQLitewill 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