SQLite on Android Made Simple: Room Persistence Library with a Touch of RxJava
Relational databases and Structured Query Language need no special introduction — anyone who’s dabbled in anything related to programming has most definitely heard of them and has probably used them to a varying degree at some point in time. And for a good reason, as they provide an efficient and relatively simple means of data persistence. Support for relational databases has been built into the Android system since it’s early days, in the form of SQLite — an embbeded database engine which enables developers to harness the power of databases without much fuss about pre-configuration, startup scripts and other chores associated with standalone, client-server SQL engines (ie. MySQL).
Even so, dealing with a SQLite database manually, or even using the legacy SQLiteOpenHelper class can be very time consuming, prone to errors and resulting in databases that are hard to maintain, modify or extend. Google recognized this problem and, in 2017, introduced the Room Persistence Library — an abstraction layer over SQLite which makes dealing with databases on Android trivially simple, even with the minimal knowledge of the SQL itself.
In the first part of this guide we’ll show you, step-by-step, how to use Room to create a simple database and perform basic CRUD operations (Create, Read, Update and Delete). In the second part we’ll try to demonstrate how we can make our code more concise and elegantly interact with the database in an asynchronus fashion with the help of awesome RxJava library.
Disclaimer — This article is a follow-up on the previous article in a series on RxJava:
Reading it is not a requirement for the first part of the article, which deals with the Room itself, but it covers some crucial RxJava basics. If you are not familiar with those make sure to check it out — it will make the second part of this article much easier to understand.
Part I: Creating a Room Database
In this part of the guide we’ll see how to set up a simple Room database, one step at a time. First steps first, so let’s add the necessary dependencies for the Room Persistence Library:
implementation 'android.arch.persistence.room:runtime:1.1.1'
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
Like so many other popular libraries, Room uses annotations, model classes and interfaces to generate the boring boilerplate code for us, allowing us to focus on the more interesting stuff. As shown in the diagram below, Room has 3 main components: Entities, Database Access Objects (DAO),and the Database itself.
Entity model class
For starters, let’s have a quick recap of how a SQL database is structured to better understand an entity.
An entity corresponds to a single database table and it represents a model class that we want to persist in a database. Each entry in a database represents an object of that class and is stored in a single row. Each column represents data relevant to that object, or, in other words, it’s member variables.
In Java, an entity is nothing more than a simple POJO class, annotated with Room annotations. An example of a simple Entity looks like this:
@Entity(tableName = "user_table")
public class User { @PrimaryKey(autoGenerate = true)
@NonNull
private int id; @ColumnInfo(name = "first_name")
private String firstName; @ColumnInfo(name = "last_name")
private String lastName; @Ignore
private boolean friend; // getters and setters...
}
Annotations @Entity and @PrimaryKey, followed by @NonNull are minimum requirements for Room to work with a model class, and pretty much self-explanatory. Even so, for those unfamiliar with the basics of SQL, we’ll take a moment to explain them anyway:
@Entity — with this annotation we make a class recognizable by Room so it can transform it’s instances into SQL table data and vice-versa.
@PrimaryKey — a primary key is a special relational database table column (or combination of columns) used to uniquely identify table records. It must contain a unique value for each row of data and it cannot be null, therefore the @NonNull annotation. It can be provided manually or auto-generated.
@ColumnInfo — this annotation allows us to use arbitrary names for table columns if, for example, we wish to use underscores for column names but avoid them in Java code.
@Ignore — have a member variable you don’t want to include in your database? Use this.
This should cover the basics of entities. Other useful entity annotations can be found browsing the Room documentation.
Note: if you’re already using a model class with other libraries that use annotated model classes, like Gson, don’t worry — you can simply add Room annotations as they don’t interfere with each other.
DAO interface
Database Access Object or DAO is usually an interface (it can also be an abstract class) which we use to interact with the database. We do so by declaring methods and annotating them with appropriate annotations. DAO should usuall cover a basic set of CRUD operations:
@Dao
public interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(User user);
@Update
void update(User user);
@Delete
void delete(User user); @Query("SELECT * FROM user_table")
List<User> getAll();
}
@Insert, @Update and @Delete are self-explanatory and completely automated by Room. They allow us to insert, update or delete a single row in the User table.
Notice that onConflict annotation parameter tells Room to replace any row already in the database when performing insert. It is also possible to insert multiple rows with a single method — we just have to use a List as a method parameter of the insert method:
@Insert
void insertAll(List<User> users);
@Query allows us to write raw SQL queries — in this case, to get all the entries from the user table (SELECT is an SQL statement for reading data from a table, followed by the * operator which means everything). Keep in mind that the method has to specify a return type we expect to get from the query. For more information about raw SQL queries and how they can be used check out this guide.
@Transaction annotation is useful when we need to perform multiple operations in one method, which is more efficient than running each operation on it’s own. For example:
@Transaction
void updateTable(List<User> users) {
clearTable()
insertAll(users)
}
That covers the basics of DAOs. All that’s left to do is connect the Entity and DAO with the Database itself.
Database class
The last but not the least step in setting up our Room database is to create an abstract class which extends the RoomDatabase class. Then we annotate it with @Database and specify one or more entity classes we made earlier to represent database tables. Also, we must declare a public abstract instance of a DAO interface we wish to use to interact with the database.
@Database(entities = User.class, version = 1)
public abstract class UserDatabase extends RoomDatabase {
public abstract UserDao userDao;
}
Now that we have everything hooked up we can get our database instance by calling a Room database builder and passing to it a context, the database class and an arbitrary name:
UserDatabase db = Room.databaseBuilder(getApplicationContext(),
UserDatabase.class, "user_db")
.build();
Note: it’s a good practice to make the RoomDatabase class a Singleton, so we don’t needlessly create more than one instance of it in our app. To learn more about the Singleton pattern and it’s implementations check out this article.
And then we can interact with the database simply by calling any method declared in the DAO, for example:
List<User> users = db.userDao().getAll();
But, if you run this code as it is, you’ll soon discover that it throws an error — Room, by default, won’t allow you to run database operations on the main thread. One way to “fix” this is to explicitly allow it, back in the database builder:
UserDatabase db = Room.databaseBuilder(getApplicationContext(),
UserDatabase.class, "user_db")
.allowMainThreadQueries()
.build();
While this works, it’s not a very good fix, since database operations can be very resource-intensive and should be run in the background. Use this approach for testing purposes only, or when dealing with a really tiny database.
There are many ways to handle background operations on Android and they each have their pros and cons. If you’ve read the previous article in this series you already know that RxJava is one of them. To take the advantage of Room’s native support for RxJava, in the second part of this guide we’ll see how we can modify our database to work alongside it and reap the benefit’s of it’s reactive streams.
Part II: Performing Database Operations with RxJava
In our previous article on RxJava we talked about a Single, how it can make our lives simpler when dealing with asynchronous tasks and make our code more concise and clean, as well as reactive. In this part of the tutorial we’ll be expanding on that, so make sure you’ve read the previous article or already know a thing or two about RxJava before proceeding.
As we mentioned already, Room comes with a built-in RxJava support. Before we can use it we have to add a few dependencies to our gradle file:
// don't forget the core RxJava dependecies
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'io.reactivex.rxjava2:rxjava:2.1.13'// Room with RxJava support
implementation 'android.arch.persistence.room:rxjava2:1.1.1'
Reading from a database
Now we can use RxJava sources — Single, Maybe and Flowable as return types in our DAOs. For querying the database we’ll stick with the Single but take your time to explore Maybe and Flowable too, as both have their benefits in certain use cases.
To use RxJava while reading from a database we simply have to modify the corresponding DAO method to return an RxJava type, in this case, a Single:
@Query("SELECT * FROM user_table")
Single<List<User>> getAll();
And then we repeat the subscription process we talked about in the previous article:
Single<List<User>> single = db.userDao().getAll();
single.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<List<User>>() {
@Override
public void onSubscribe(Disposable d) {
// add it to a CompositeDisposable
}
@Override
public void onSuccess(List<User> users) {
// update the UI
} @Override
public void onError(Throwable e) {
// show an error message
}
});
Or, if we feel like being more concise, do the same thing with Lambda expressions (just don’t forget to clean up the disposable afterwards):
Single<List<User>> single = db.userDao().getAll();
single.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(users -> // update the UI,
throwable -> // show an error message
);// add it to a CompositeDisposable
Much cleaner, isn’t it? While we’re at it, let us introduce a new RxJava operator — Map. Let’s say we want to add another user to our list of users immadiately after fetching it from the database. Map is just the thing to help us achieve this in a clean way, while staying in the same reactive stream.
Map transforms the emitted list of users by applying a function to it and returns it back to the stream. As simple as this:
single.map(users -> {
users.add(new User("Sam", "Lowry");
return users;
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(users -> // update the UI,
throwable -> // show an error message
);
Neat. But, enough about that — let’s see how we can perform the remaining database operations with the help of RxJava.
Writing to a database
Single is fine when we need to query a database for data, but what about insert, update or delete? They are just methods to perform an operation, in most cases without a return value, so Single doesn’t quite fit here.
Luckily, RxJava has us covered — A Completable is similar to a Single, only without a return value. It can run a piece of code in the background and either complete with success or throw an error. Here is an example of a Completable:
Completable.fromAction(() -> db.userDao().insert(user)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new CompletableObserver() {
@Override
public void onSubscribe(Disposable d) {
// just like with a Single
}
@Override
public void onComplete() {
// action was completed successfully
}
@Override
public void onError(Throwable e) {
// something went wrong
}
});
As you can see the approach is almost the same, the major difference being, in this case, that we created a Completable ourselves, by calling the static fromAction() method, to which we then passed a lambda to perform a database operation.
Just like with a Single, you can use lambdas instead of a new CompletableObserver in the subscribe() if you so prefer:
Completable.fromAction(() -> db.userDao().insert(user))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(()-> // completed with success,
throwable -> // there was an error
);
And that’s that folks — with this you should have enough basic knowledge to create and use a simple Room database with RxJava. We hope you’ve enjoyed this tutorial and found it at least a bit helpful. Don’t forget to visit the official sites of Room and RxJava to expand on the basics learned here — it will be worth your time.
Also, If you want to get a better insight into how this stuff works in a simple MVP app check out my Github repo.
Any questions, comments or suggestions are welcome, so feel free to post them below.