
Room is persistence library introduced at Google I/O 2017 which provides an abstraction layer over the SQLite database which in turn provides enhanced security, easy access, easy to setup and quick to get started with new database.
Let's look at major problems with SQLite:
Room is an annotation processing based SQL object mapping library provided by Google. Room is designed to abstract away the underlying database tables and queries. Therefore it provides an easy way to create and use Sqlite database. It is based on best-practices for persisting data in databases. For example, Room does, by default, not allow database access in the main thread. It also offers compile time check for the table name, column name and live monitoring for database changes using LiveData.
Room does not support object references between entities, to avoid potential performances issues. Even though you cannot use direct relationships, it is possible to define foreign key constraints between entities.
Add the following repository to your gradle (project) file.
allprojects {
repositories {
...
maven { url 'https://maven.google.com' }
}
}
Add the following library and annotation processor to your gradle (app) file.
compile 'com.android.support:appcompat-v7:26.1.0' compile 'com.android.support:recyclerview-v7:26.1.0' compile "android.arch.persistence.room:runtime:1.0.0" annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
The reason why annotation processor is needed is because all operations like Insert, Delete, Update etc are annotated.
Components of Room:
@Entity where all the variable will becomes column name for the table and name of the model class becomes name of the table.These components, along with their relationships with the rest of the app, appear in following figure
Entity
First of all let’s create our model class where all it’s variable name will become the column name and name of model class will become the table name.
@Entity
public class Movie {
@PrimaryKey(autoGenerate = true)
private long id;
private String title;
private int year;
@Embedded(prefix = "rating")
Rating rating;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public Rating getRating() {
return rating;
}
public void setRating(Rating rating) {
this.rating = rating;
}
@Override
public String toString() {
return "Movie{" +
"title='" + title + '\'' +
", year=" + year +
'}';
}
}
public class Rating {
private int id;
private float Imdb;
private float Tmdb;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public float getImdb() {
return Imdb;
}
public void setImdb(float imdb) {
Imdb = imdb;
}
public float getTmdb() {
return Tmdb;
}
public void setTmdb(float tmdb) {
Tmdb = tmdb;
}
}
The table name will be Movie. The column names will be id, title, year, ratingImdb, ratingTmdb.
Room provides several annotations to define entities.
PrimaryKey marks a field as primary key field.ForeignKey sets foreign key constraint.Ignore ignores marked field, won’t be part of table.ColumnInfo specifies column name in database, instead of using field name as column name.Index creates index to speed up queries.Embedded marks a field as embedded that makes all subfields of the embedded class as columns of entity.Relation specifies relations, useful to fetch related entities.Variables defined inside model class becomes the column name by default but there are few operation that can be performed.
column_name annotation to change the name for the column if you don’t want to use your variable name as a column name.autogenerate = true while defining primary key, your data type should be of type int or long. Even when there is only 1 field, you still need to annotate the field with the @PrimaryKey annotation.If you have object of some other model class where that also contains same variable name than just annotate that object with @Embedded(prefix="some_name"). This will add your prefix to all the variable name for that class.
If you want your data to get stored in some other format than that of the type of variable you defined in your model class than create another class add your conversion logic there and annotate those methods with @TypeConverter than add that class to your database class using annotation @TypeConverters.
Each entity must define at least 1 field as a primary key. If the entity has a composite primary key, you can use the primaryKeys property of the @Entity annotation, as shown in the following code snippet:
@Entity(primaryKeys = {"title", "year"})
class Movie {
public String title;
public int year;
@Ignore
Bitmap poster;
}
By default, Room uses the class name as the database table name. If you want the table to have a different name, set the tableName property of the @Entity annotation, as shown in the following code snippet:
@Entity(tableName = "movies")
class Movie {
...
}
Depending on how you access the data, you might want to index certain fields in the database to speed up your queries. To add indices to an entity, include the indices property within the @Entity annotation, listing the names of the columns that you want to include in the index or composite index. The following code snippet demonstrates this annotation process:
@Entity(indices = {@Index("name"), @Index(value = {"title", "year"})})
class Movie {
@PrimaryKey
public int id;
public String title;
public int year;
@ColumnInfo(name = "origin")
public String country;
@Ignore
Bitmap poster;
}
Dao
To access your app's data using the Room persistence library, you work with data access objects, or DAOs. This set of Dao objects forms the main component of Room, as each DAO includes methods that offer abstract access to your app's database.
There are four annotations @Query, @Insert, @Update, @Delete to perform CRUD operations. @Query annotation is used to perform read operation on database.
@Query is the main annotation used in DAO classes. It allows you to perform read/write operations on a database. Each @Query method is verified at compile time, so if there is a problem with the query, a compilation error occurs instead of a runtime failure.
Room also verifies the return value of the query such that if the name of the field in the returned object doesn't match the corresponding column names in the query response, Room alerts you in one of the following two ways:
Simple queries
@Dao
public interface MovieDao {
@Query("SELECT * FROM Movie")
public User[] loadAllMovies();
}
Most of the time, you need to pass parameters into queries to perform filtering operations, such as displaying only movies who are older than a certain year. To accomplish this task, use method parameters in your Room annotation, as shown in the following code snippet:
@Dao
public interface MovieDao {
@Query("SELECT * FROM Movie WHERE year > :year")
public User[] loadAllMoviesOlderThan(int year);
}
Now let’s create our Dao interface which contains the list of operation that we would like to perform on table.
@Dao
public interface MovieDao {
@Insert
void insert(Movie... movies);
@Query("SELECT COUNT(*) FROM Movie")
int count();
@Query("SELECT * FROM Movie")
List<Movie> fetchAllData();
@Query("SELECT * FROM Movie WHERE id =:id")
Movie getSingleRecord(int id);
@Update
void update(Movie... movies);
@Delete
void delete(Movie... movies);
@Query("DELETE FROM Movie")
public void clear();
}
Room does not allow accessing the database on the main thread unless you called allowMainThreadQueries() on the builder because it might potentially lock the UI for a long periods of time. Asynchronous queries (queries that return LiveData or RxJava Flowable) are exempt from this rule since they asynchronously run the query on a background thread when needed.
You can run room on background thread using AsyncTask. This way of running room gives you an opportunity to perform tasks on main thread after room dao background operation is complete.
private void insertMovie() {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
db.movieDao().insert(movie4);
return null;
}
}.execute();
}
private void loadMovies() {
new AsyncTask<Void, Void, List<Movie>>() {
@Override
protected List doInBackground(Void... params) {
return db.movieDao().fetchAllData();
}
@Override
protected void onPostExecute(List items) {
adapter.setItems(items);
}
}.execute();
}
Below codes show how to create executor and use it to call DAO methods on background thread. But this way of calling room on background thread can be used only if you don’t need to perform any operation on main thread after database operation is complete.
final Executor executor = Executors.newFixedThreadPool(2);
MovieDao dao = db.movieDao();
public void addMovie(Movie m){
executor.execute(() -> {
dao.insert(m);
});
}
Database
Create a database holder called HollywoodDatabase extends RoomDatabase, we will define list of entities and database version. Class is annotated with @Database annotation. It is good practice to use singleton approach for the database, so you need to create an static method which will return instance of HollywoodDatabase.
@Database(entities = {Movie.class}, version = 1)
public abstract class HollywoodDatabase extends RoomDatabase {
private static HollywoodDatabase db;
public abstract MovieDao movieDao();
public static HollywoodDatabase getAppDatabase(Context context) {
if (db == null) {
db = Room.databaseBuilder(context.getApplicationContext(), HollywoodDatabase.class, "movie-db")
// don't do this on a real app!
.allowMainThreadQueries()
.build();
}
return db;
}
public static void destroyInstance() {
db = null;
}
}
Integration
Now we are all set to perform operation on our database. So let’s add some records to the table
public class MainActivity extends AppCompatActivity {
private String TAG = MainActivity.class.getSimpleName();
HollywoodDatabase db;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
db = HollywoodDatabase.getAppDatabase(this);
// add movie
Movie movie1 = new Movie();
movie1.setTitle("The Godfather");
movie1.setYear(1972);
Rating rating1 = new Rating();
rating1.setImdb(9.2f);
rating1.setTmdb(8.5f);
movie1.setRating(rating1);
db.movieDao().insert(movie1);
Movie movie2 = new Movie();
movie2.setTitle("The Dark Knight");
movie2.setYear(2008);
Rating rating2 = new Rating();
rating2.setImdb(9.0f);
rating2.setTmdb(8.3f);
movie2.setRating(rating2);
long movieId2 = db.movieDao().insert(movie2);
movie2.setId(movieId2);
showMovies();
// update movie
movie2.setTitle("The Godfather I");
db.movieDao().update(movie2);
showMovies();
// delete movie
db.movieDao().delete(movie2);
showMovies();
}
private void showMovies() {
List<Movie> movies = db.movieDao().fetchAllData();
for (Movie movie : movies) {
Log.d(TAG, "Movie: " + movie);
}
}
@Override
protected void onDestroy() {
HollywoodDatabase.destroyInstance();
super.onDestroy();
}
}
Define relationships between objects
Because SQLite is a relational database, you can specify relationships between objects. Even though most object-relational mapping libraries allow entity objects to reference each other, Room explicitly forbids this. To learn about the technical reasoning behind this decision, see Understand why Room doesn't allow object references.
Even though you cannot use direct relationships, Room still allows you to define Foreign Key constraints between entities.
For example, if there's another entity called Artist, you can define its relationship to the Movie entity using the @ForeignKey annotation, as shown in the following code snippet:
@Entity(foreignKeys = @ForeignKey(entity = Movie.class,
parentColumns = "id",
childColumns = "movie_id"))
class Artist {
@PrimaryKey
public int id;
public String name;
@ColumnInfo(name = "movie_id")
public int movieId;
}
Foreign keys are very powerful, as they allow you to specify what occurs when the referenced entity is updated. For instance, you can tell SQLite to delete all books for a user if the corresponding instance of User is deleted by including onDelete = CASCADE in the @ForeignKey annotation.
Type converters
So far, all of our fields have been basic primitives (int, float, etc.) or String. There is a good reason for that: those are all that Room understands "out of the box". Everything else requires some amount of assistance on our part (Date, Location, etc).
Type converters are a pair of methods, annotated with @TypeConverter, that map the type for a single database column to a type for a Java field. So, for example, we can:
Date field to a Long, which can go in a SQLite INTEGER columnLocation field to a String, which can go in a SQLite TEXT columnString values to a single String (e.g., comma-separated values), which can go in a SQLite TEXT columnHowever, type converters offer only a 1:1 conversion: a single Java field to and from a single SQLite column. If you have a single Java field that should map to several SQLite columns, the @Embedded approach can handle that
First, define a Java class somewhere.
@Entity(tableName = "movies")
class Movie {
...
}
Next, for each type to be converted, create two public static methods that convert from one type to the other. So for example, you would have one public static method that takes a Date and returns a Long (e.g., returning the milliseconds since the Unix epoch value), and a counterpart method that takes a Long and returns a Date. If the converter method is passed null, the proper result is null.
Then, each of those methods get the @TypeConverter annotation. The method names do not matter, so pick a convention that works for you.
Finally, you add a @TypeConverters annotation, listing this and any other type converter classes, to... something. What the "something" is controls the scope of where that type converter can be used.
A typical way of storing a date/time value in a database is to use the number of milliseconds since the Unix epoch (i.e., the number of milliseconds since midnight, 1 January 1970). Date has a getTime() method that returns this value.
So, let's define DataTransformer class that contains four methods, each annotated with @TypeConverter, for converting Date to and from a Long and Location to from String:
public class DataTransformer {
@TypeConverter
public static Long fromDate(Date date) {
if (date == null)
return(null);
return(date.getTime());
}
@TypeConverter
public static Date toDate(Long millisSinceEpoch) {
if (millisSinceEpoch == null)
return(null);
return(new Date(millisSinceEpoch));
}
@TypeConverter
public static String fromLocation(Location location) {
if (location==null)
return(null);
return(String.format(Locale.US, "%f,%f", location.getLatitude(),
location.getLongitude()));
}
@TypeConverter
public static Location toLocation(String latlon) {
if (latlon==null)
return(null);
String[] pieces=latlon.split(",");
Location result=new Location("");
result.setLatitude(Double.parseDouble(pieces[0]));
result.setLongitude(Double.parseDouble(pieces[1]));
return(result);
}
}
HollywoodDatabase then has the @TypeConverters annotation, listing DataTransformer as the one class that has type conversion methods:
@Database(entities = {Movie.class}, version = 1)
@TypeConverters({DataTransformer.class})
public abstract class HollywoodDatabase extends RoomDatabase {
private static HollywoodDatabase db;
public abstract MovieDao movieDao();
public static HollywoodDatabase get(Context context, boolean isMemory) {
if (db == null) {
if (isMemory) {
db = Room.inMemoryDatabaseBuilder(context.getApplicationContext(),
HollywoodDatabase.class);
} else {
db = Room.databaseBuilder(context.getApplicationContext(), HollywoodDatabase.class, "movie-db")
// don't do this on a real app!
.allowMainThreadQueries()
.build();
}
}
return db;
}
public static void destroyInstance() {
db = null;
}
}
Now, classes like Movie can use Date fields, which will be stored in INTEGER columns in the database.
Pre-populate Room database
Sometimes is very helpful have a pre-loaded data to start to use an application that uses a database as storing data. Room has a way to pre-populate the database at first run. The trick is override the onCreate method in Room callback.
db = Room.databaseBuilder(context.getApplicationContext(),
HollywoodDatabase.class,
"movie-db")
.addCallback(new Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
Executors.newSingleThreadScheduledExecutor().execute(new Runnable() {
@Override
public void run() {
List<Movie> items = new ArrayList<>();
Movie movie1 = new Movie();
movie1.setTitle("The Godfather");
...
items.add(movie1);
Movie movie2 = new Movie();
movie2.setTitle("The Dark Knight");
...
items.add(movie2);
getAppDatabase(context).movieDao().insert(items);
}
});
}
})
.build();
Room Database Migration
It is very common in any application that database tables are altered to introduce new features in applications. If your app uses Room and database tables need to be altered, then you should use migration process to retain data and make application continue to work.
Here’s what happens when you access the database for the first time:
SQLiteOpenHelper.onUpgrade method is called and Room triggers the migrationsTypically, migration is an anonymous inner classes that handle the conversion from some older schema to a newer one. You pass a bunch of Migration instances to Room, representing different pair-wise schema upgrade paths. Room then determines which one(s) need to be used at any point in time, to update the schema from whatever it was to whatever it needs to be.
A Migration itself has only one required method: migrate(). You are given a SupportSQLiteDatabase, which resembles a SQLiteDatabase and allows you to execute SQL statements to modify the schema as needed.
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE Movie ADD COLUMN comments INTEGER default 0 NOT NULL");
}
};
The Migration constructor takes two parameters: the old schema version number and the new schema version number. Hence, the recommended pattern is to use anonymous inner classes, where you can provide the migrate() method to use for migrating the schema between that particular pair of schema versions.
Simply creating a Migration as a static field somewhere is necessary but not sufficient to have Room know about performing the migration. Instead, you need to use the addMigrations() method on RoomDatabase.Builder to teach Room about your Migration objects. addMigrations() accepts a varargs, and so you can pass in one or several Migration objects as needed.
@Database(entities = {Movie.class}, version = 2)
public abstract class HollywoodDatabase extends RoomDatabase {
private static HollywoodDatabase db;
...
public static HollywoodDatabase getAppDatabase(Context context) {
if (db == null) {
db = Room.databaseBuilder(context.getApplicationContext(), HollywoodDatabase.class, "movie-db")
.allowMainThreadQueries()
.addMigrations(MIGRATION_1_2)
.build();
}
return db;
}
...
}
If you don’t want to provide migrations and you specifically want your database to be cleared when you upgrade the version, call fallbackToDestructiveMigration in the database builder:
db = Room.databaseBuilder(context.getApplicationContext(), HollywoodDatabase.class, "movie-db")
.allowMainThreadQueries()
.fallbackToDestructiveMigration()
.build();
Kotlin && Room && RxJava
Add Room dependencies in module build.gradle:
apply plugin: 'kotlin-kapt'
...
dependencies {
...
// for room
implementation "android.arch.persistence.room:runtime:1.0.0"
implementation "android.arch.persistence.room:rxjava2:1.0.0"
kapt "android.arch.persistence.room:compiler:1.0.0"
// for rxJava
implementation "io.reactivex.rxjava2:rxjava:2.1.12"
implementation "io.reactivex.rxjava2:rxandroid:2.0.2"
}
Add Google’s Maven Repo in the project build.gradle:
allprojects {
repositories {
...
google()
}
}
Create an Entity data class, for this, we need to annotate it with @Entity. By default, Room creates a column for each field that’s defined in the entity. If an entity has fields that you don’t want to persist, you can annotate them using @Ignore.
@Entity
data class Movie(
@PrimaryKey(autoGenerate = true)
val id: Long,
val name: String,
val year: Int
)
Create a DAO using an interface and the @Dao annotation :
@Dao
interface MovieDao {
@Query("SELECT * FROM Movie")
fun getAllItems(): Flowable<List<Movie>>
@Query("SELECT * FROM Movie")
fun getAll(): Single<List<Movie>>
@Query("SELECT COUNT(id) FROM Movie")
fun count(): Single<Long>
@Query("SELECT * FROM Movie WHERE id = :id")
fun getItemById(id: String): Single<Movie>
@Insert(onConflict = REPLACE)
fun insert(movie: Movie)
@Delete
fun delete(movie: Movie)
}
We can use different types of observables.
Maybe<Movie>.
Here’s what happens:
Maybe will complete.Maybe will trigger onSuccess and it will complete.Maybe was completed, nothing happens.Single<Movie>.
Here’s what happens:
Single will trigger onError(EmptyResultSetException.class).Single will trigger onSuccess.Single.onComplete was called, nothing happens, since the stream was completed.Flowable<Movie>.
Here’s what happens:
Flowable will not emit, neither onNext, nor onError.Flowable will trigger onNext.Flowable object will emit automatically, allowing you to update the UI based on the latest data.Create a Database holder that extends RoomDatabase. The class needs to be annotated with @Database annotation.
@Database(entities = arrayOf(Movie::class), version = 1)
abstract class MovieDatabase : RoomDatabase() {
abstract fun movieDao(): MovieDao
}
Room Database instance is expensive, so we can create the AppDatabase object in the Application object and access it where need it or take the standard singleton approach.
class MyApp : Application() {
companion object {
var database: MovieDatabase? = null
}
override fun onCreate() {
super.onCreate()
MyApp.database = Room.databaseBuilder(this, MovieDatabase::class.java, "movies.db").build()
}
}
Room enforces database access to be off the main thread, since database operations can be potentially long-running. Using Rx we can easily move the database call to a background I/O thread.
To insert into the database, I’ve used an RxJava Single to run this action asynchronously:
fun addMovie(name: String, year: Int) {
val item = Movie(0, name, year)
Single.fromCallable {
MyApp.database?.movieDao()?.insert(item)
}.subscribeOn(Schedulers.io())
.subscribe()
}
Using the dao method that returns a Flowable, we can also set up a subscriber to get updates to the database as they come in.
MyApp.database?.movieDao()?.getAllItems()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { items ->
// do something with 'users'
}
Following is my Activity.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// add items
for (i in 1..5) {
addMovie("Movie $i", 2000 + i)
}
// count items
MyApp.database?.movieDao()?.count()
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe { count ->
Log.d("TAG", "Total: $count")
}
// list all
MyApp.database?.movieDao()?.getAll()
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe { items ->
Log.d("TAG", items.toString())
}
}
fun addMovie(name: String, year: Int) {
val item = Movie(0, name, year)
Single.fromCallable {
MyApp.database?.movieDao()?.insert(item)
}.subscribeOn(Schedulers.io())
.subscribe()
}
}