Note: This article is part of the advanced Room series which covers all the details about the Room persistence library. You can read all the articles here:
- Introduction to Room Persistent Library in Android
- Data Access Objects — DAO in Room
- Entity Relationship in Room
- How does Room work internally? [You are here]
- Room Database Migrations
- Using Room with LiveData and other third-party libraries
How Room works internally?
In the previous articles, we discussed how we can use Room library (part of Google’s Jetpack project) to create relational persistence in Android applications. Room makes it very easy for a developer to setup a database and start using it in production.
In this article, we are going to focus on how Room accomplishes all these things.
We are going to use this project as reference for explaining how Room actually does everything. Following are the highlights of our project:
- It has a single database(UserDatabase) which contains only one table/entity(User).
- User table has 3 columns: uid, first name and last name
- UserDao is the interface through which our application interacts with the database.
Internal Working of Room
After creating a Room Database, the first time you compile your code, Room autogenerates implementation of your @Database
and @Dao
annotated classes. In the above example, implementation of UserDatabase
and UserDao
is autogenerated by Room annotation processor.
Note: You can find the autogenerated code in build/generated/source/kapt/ folder.
In our example, implementation of UserDatabase
is named as UserDatabase_Impl
and implementation of UserDao
is named as UserDao_Impl
. These are the classes where actual processing happens. Let’s discuss both of the implementations individually.
UserDatabase_Impl
An overview of UserDatabase_Impl looks like this:
createOpenHelper()
is invoked when you build instance of your database usingRoom.databaseBuilder().build()
. It creates and returns an instance ofSupportSQLiteOpenHelper
which is a helper class for managing database creation and version management.createInvalidationTracker()
creates an invalidation tracker which keeps a list of tables modified by queries and notifies its callbacks about these tables.clearAllTables()
implements the behaviour of deleting data from all the tables of the specified database.userDao()
creates(if not exists) and returns the instance ofUserDao_Impl
for interacting with theusers
table.
UserDao_Impl
UserDao_Impl implements all the methods in UserDao. The overview of UserDao_Impl looks like this:
In the above example, UserDao_Impl
has 3 fields: __db, __insertionAdapterOfUser and __deletionAdapterOfUser.
- __db is an instance of RoomDatabase which is used for multiple purposes like transaction and querying the database.
- __insertionAdapterOfUser is an instance of EntityInsertionAdapter used for inserting entities into a table. This is used in
insertAll()
method. - __deletionAdapterOfUser is an instance of EntityDeletionOrUpdateAdapter used to update/delete entities from a table. This is used in
delete()
method.
Building the RoomDatabase
Till now, we have understood what happens after our project is successfully compiled. Also we know that we need an instance of UserDatabase
which gives us an instance of UserDao
in order to perform any database related operations.
To get an instance of UserDatabase,
Room provides us a builder method named Room.databaseBuilder
which gives us an instance of RoomDatabase.Builder
. We can use this instance to get UserDatabase
by invoking the build()
method.
We can use this builder to configure our database like
createFromAsset()
/createFromFile()
to create and open database from an asset(located in the application ‘assets/’ folder)/a pre-packaged database file.addMigrations()
to add database migration from one version to another. A migration is needed whenever we are changing the version of our database even if there is no change in schema of both versions.allowMainThreadQueries()
to allow making database queries from main thread. By default, Room doesn’t allow this.fallbackToDestructiveMigration()
allows Room to destructively recreate database tables if migration is not found.
There are also many other methods provided in RoomDatabase.Builder
for database configuration.
Once we invoke build()
method on thisRoomDatabase.Builder
instance, Room validates and creates an instance of the autogenerated implementation of UserDatabase::class.java
— i.e., UserDatabase_Impl
. After the creation of UserDatabase_Impl
, init()
method is invoked on the database by passing the database configuration which in turn invokes the createOpenHelper()
method of UserDatabase_Impl
.
Now we are going to discuss the implementation of some important methods in UserDatabase_Impl and UserDao_Impl discussed earlier.
userDao() in UserDatabase_Impl
@Override
public UserDao userDao() {
if (_userDao != null) {
return _userDao;
} else {
synchronized(this) {
if(_userDao == null) {
_userDao = new UserDao_Impl(this);
}
return _userDao;
}
}
}
It lazily creates the implementation of UserDao
— i.e., UserDao_Impl
and returns it whenever userDao()
is invoked. As we can see, it passes the instance of RoomDatabase
in UserDao_Impl’s
constructor.
insertAll() in UserDao_Impl
@Override
public void insertAll(final User... users) {
__db.assertNotSuspendingTransaction();
__db.beginTransaction();
try {
__insertionAdapterOfUser.insert(users);
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
It uses __db for creating the transaction and __insertionAdapterOfUser for insertion.
delete() in UserDao_Impl
@Override
public void delete(final User user) {
__db.assertNotSuspendingTransaction();
__db.beginTransaction();
try {
__deletionAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
It uses __db for creating the transaction and __deletionAdapterOfUser for deletion.
getAll() in UserDao_Impl
@Override
public List<User> getAll() {
final String _sql = "SELECT * FROM users";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
__db.assertNotSuspendingTransaction();
final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
try {
final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
final int _cursorIndexOfFirstName = CursorUtil.getColumnIndexOrThrow(_cursor, "first_name");
final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "last_name");
final List<User> _result = new ArrayList<User>(_cursor.getCount());
while(_cursor.moveToNext()) {
final User _item;
final int _tmpUid;
_tmpUid = _cursor.getInt(_cursorIndexOfUid);
final String _tmpFirstName;
_tmpFirstName = _cursor.getString(_cursorIndexOfFirstName);
final String _tmpLastName;
_tmpLastName = _cursor.getString(_cursorIndexOfLastName);
_item = new User(_tmpUid,_tmpFirstName,_tmpLastName);
_result.add(_item);
}
return _result;
} finally {
_cursor.close();
_statement.release();
}
}
As we can see, it creates a RoomSQLiteQuery
object from the query specified in @Query
annotation. It then simply creates a cursor to fetch data from the database.
This is enough to get a basic understanding of how Room works internally.
Thank you!!!