How does Room work internally?

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:

So, let's get started.

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.

You can find the project that is going to be used in the blog from here.

We are going to use this project as a 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).
@Database(entities = [User::class], version = 1)
abstract class UserDatabase : RoomDatabase() {
 abstract fun userDao(): UserDao
}
  • User table has 3 columns: uid, first name and last name
@Entity(tableName = USERS_TABLE)
data class User(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = FIRST_NAME_COLUMN) val firstName: String?,
    @ColumnInfo(name = LAST_NAME_COLUMN) val lastName: String?
)
  • UserDao is the interface through which our application interacts with the database.
@Dao
interface UserDao {
    @Query("SELECT * FROM $USERS_TABLE")
    fun getAll(): List<User>

    @Insert
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)
}

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, the 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:

public final class UserDatabase_Impl extends UserDatabase {
  private volatile UserDao _userDao;

  @Override
  protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
    //Implementation
  }

  @Override
  protected InvalidationTracker createInvalidationTracker() {
    //Implementation
  }

  @Override
  public void clearAllTables() {
    //Implementation
  }

  @Override
  public UserDao userDao() {
    //Implementation
  }
}
  • 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:

public final class UserDao_Impl implements UserDao {
  private final RoomDatabase __db;

  private final EntityInsertionAdapter<User> __insertionAdapterOfUser;

  private final EntityDeletionOrUpdateAdapter<User> __deletionAdapterOfUser;

  public UserDao_Impl(RoomDatabase __db) {
    this.__db = __db;
    this.__insertionAdapterOfUser = new EntityInsertionAdapter<User>(__db) {
      //Implementation
    };
    this.__deletionAdapterOfUser = new EntityDeletionOrUpdateAdapter<User>(__db) {
      //Implementation
    };
  }

  @Override
  public void insertAll(final User... users) {
    //Implementation
  }

  @Override
  public void delete(final User user) {
    //Implementation
  }

  @Override
  public List<User> getAll() {
    //Implementation
  }

  @Override
  public List<User> loadAllByIds(final int[] userIds) {
    //Implementation
  }

  @Override
  public User findByName(final String first, final String last) {
    //Implementation
  }
}

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 the 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 with 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.

val userDatabase = Room.databaseBuilder(
    applicationContext,
    UserDatabase::class.java,
    "users-db"
).build()

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 the 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 this RoomDatabase.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. Hope you enjoyed this blog. In the next blog, we are going to learn Room Database Migrations.

You can also connect with me on LinkedIn, Twitter, Facebook and Github.

Thank You!!!