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() } }