How does Room work internally?

Deepanshu
MindOrks
Published in
4 min readMay 23, 2020

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:

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 using Room.databaseBuilder().build(). It creates and returns an instance of SupportSQLiteOpenHelper 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 of UserDao_Impl for interacting with the users 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!!!

--

--