Internal workings of Room Database library — Part 1
Tanay Tandon on 2024-02-09
Internal workings of Room Database library — Part 1
Room is perhaps the most common database library out there for native Android App Development. It is easy to use and using just a few lines of code allows us to create a database, create tables in the database and fetch the data from the database. In this blog post we will try to examine how Room works internally.
We will see have goes behind the scenes when
- Insert a new entity
- Update an existing entity
- Delete an existing entity
- Fetch an entity.
To examine Room internals this we will create the database module of a simple ToDo app. The ToDo app allows users to view existing entries, create a new entry, update an existing entry, and delete an existing entry. All the data will be saved locally in an SQLite database.
We have to create the following classes
- ToDoItem representing the database table where all the ToDo Items are saved. Marked with the Entity annotation.
- DbDao interface which encapsulates all the logic to insert, update, delete and fetch the items. Marked with the Dao annotation.
- AppDatabase corresponding to the SQLite database. Marked with the Database annotation.
@Dao interface DbDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertItem(item: ToDoItem): Long @Update(entity = ToDoItem::class) suspend fun updateItem(item: ToDoItem) @Delete(entity = ToDoItem::class) suspend fun removeItem(item: ToDoItem) @Query("select * from ToDoItem order by createdAt") fun fetchItems(): Flow<List<ToDoItem>> }
@Database(entities = [ToDoItem::class], version = 1) abstract class AppDatabase { abstract fun getDao(): DbDao }
After creating the above classes Room will take care of creating the SQLite database and the required tables and the DbDao class can be used to insert, update, remove, fetch ToDoItem.
Usage of the DbDao class will look like
class AppRepo constructor(private val dao: DbDao) { suspend fun insertItem(item: ToDoItem) { dao.insertItem(item) } suspend fun updateItem(item: ToDoItem) { dao.updateItem(item) } suspend fun removeItem(item: ToDoItem) { dao.removeItem(item) } fun fetchItems(): Flow<List<ToDoItem>> = dao.fetchItems() }
To provide an instance of DbDao we must call the method getDao of the AppDatabase class. To call the getDao method we must get an instance of the AppDatabase class.
fun appDbInstance(context: Context): AppDatabase = Room.databaseBuilder(context, AppDatabase::class.java, "app_db").build()
Now that we have created all the required classes we can being the investigation of Room internals.
We will try to answer the following questions: 1) What happens internally when we call Room.databaseBuilder.build ?
2) DbDao is an interface and an interface cannot be instantiated. To get an instance of DbDao it must be getting extended and that extended class is returned when we call AppDatabase.getDao. When and where is this instance created?
Once we have answered the above questions we can see how Room actually inserts, updates, removes, and fetches items.
What happens internally when we call Room.databaseBuilder.build?
The method databaseBuilder returns an instance of the open class Builder. Builder is the inner class of RoomDatabase and the constructor of Builder class is internal meaning the constructor cannot be called outside the Room library module.
The build method creates the instance of the AppDatabase class and returns it. The following lines deal with the instance creation logic.
val db = Room.getGeneratedImplementation<T, T>(klass, "_Impl") db.init(configuration) return db
The call to the method getGeneratedImplementation suggests that Room generates a class extending the AppDatabase class.
Build the project and after the build is successfully completed navigate to <module_name>/build/generated/ksp/<build_flavor>/java. You should see package structure similar to one we created.
The class AppDatabase_Impl is generated by Room at build time. It extends the AppDatabase class and encapsulates all the logic associated with creating the SQLite database.
We can conclude at build time Room generates a class extending the abstract database class we created. This class encapsulates all the logic associated with creating the corresponding SQLite database. This class provides the SQLite database instance which is used to modify the data. Call to Room.databasebuilder.build returns an instance of this generated class.
How is DbDao instantiated?
As we can see above Room generates a class DbDao_Impl this class extends the DbDao class. The class DbDao_Impl extends all the methods of the DbDao class and these methods contain the logic to perform the required database operations.
AppDatabase_Impl class has a private class member of type DbDao. The method getDao of the class AppDatabaseImpl checks if the class member is null, if null it creates an instance and assigns it to the class member and this non null value is then returned.
// method getDao of AppDatabase @Override public DbDao getDao() { if (_dbDao != null) { return _dbDao; } else { synchronized(this) { if(_dbDao == null) { _dbDao = new DbDao_Impl(this); } return _dbDao; } } }
Now we will look at the DbDao_Impl class. Corresponding to our DbDao class the DbDao_Impl class extends the four methods and has four private class members.
private final RoomDatabase __db; private final EntityInsertionAdapter<ToDoItem> __insertionAdapterOfToDoItem; private final EntityDeletionOrUpdateAdapter<ToDoItem> __deletionAdapterOfToDoItem; private final EntityDeletionOrUpdateAdapter<ToDoItem> __updateAdapterOfToDoItem; @Override public Object insertItem(final ToDoItem item, final Continuation<? super Long> $completion) { // uses __insertionAdapterOfToDoItem } @Override public Object removeItem(final ToDoItem item, final Continuation<? super Unit> $completion) { // uses __deletionAdapterOfToDoItem } @Override public Object updateItem(final ToDoItem item, final Continuation<? super Unit> $completion) { // uses __updateAdapterOfToDoItem } @Override public Flow<List<ToDoItem>> fetchItems() { }
EntityDeletionOrUpdateAdapter and EntityInsertionAdapter are abstract classes extending the abstract class SharedSQLiteStatement.
SharedSQLiteStatement has the abstract method createQuery
protected abstract fun createQuery(): String
EntityDeletionOrUpdateAdapter encapsulates the logic for deleting and updating a RoomEntity. It corresponds to the Update and Delete annotations. It has abstract method bind.
protected abstract fun bind(statement: SupportSQLiteStatement, entity: T)
EntityInsertionAdpater encapsulates the logic for inserting a new Room Entity. It corresponds to the Insert annotation and declares the abstract method bind.
protected abstract fun bind(statement: SupportSQLiteStatement, entity: T)
Note there is no adapter class corresponding to Query annotation.
What happens inside the insertItem method?
In the constructor of DbDao_Impl the private member ___insertionAdapterOfToDoItem is assigned an instance of EntityInsertionAdapter. The createQuery method returns an parametrized query and the bind function holds logic to assign parameters to the query returned by createQuery function.
this.__insertionAdapterOfToDoItem = new EntityInsertionAdapter<ToDoItem>(__db) { @Override @NonNull protected String createQuery() { return "INSERT OR REPLACE INTO `ToDoItem` (`id`,`description`,`isComplete`,`createdAt`) VALUES (nullif(?, 0),?,?,?)"; } @Override protected void bind(@NonNull final SupportSQLiteStatement statement, @NonNull final ToDoItem entity) { statement.bindLong(1, entity.getId()); statement.bindString(2, entity.getDescription()); final int _tmp = entity.isComplete() ? 1 : 0; statement.bindLong(3, _tmp); statement.bindLong(4, entity.getCreatedAt()); } };
In the body of the insertItem method of DbDao_Impl the method insertAndReturnId of EntityInsertionAdapter is called with the passed ToDoItem instance.
final Long _result = __insertionAdapterOfToDoItem.insertAndReturnId(item);
The inside the insertAndReturnId method, createQuery method is first called followed by bind method. This call sequence results in a SQLite query which is then executed to insert the entity.
/** * Inserts the given entity into the database and returns the row id. * * @param entity The entity to insert * @return The SQLite row id or -1 if no row is inserted */ fun insertAndReturnId(entity: T): Long { val stmt: SupportSQLiteStatement = acquire() // call to accquire results in call to createQuery return try { bind(stmt, entity) stmt.executeInsert() } finally { release(stmt) } }
The bodies of the methods removeItem and updateItem are similar to the body of the insertItem method. They call the handle method of the EntiyUpdationOrDeletionAdapter class.
fun handle(entity: T): Int { val stmt: SupportSQLiteStatement = acquire() return try { bind(stmt, entity) stmt.executeUpdateDelete() } finally { release(stmt) } }
The body of the fetchItems method requires discussing a couple of other classes and I will discuss it in a future blog post.
Thanks for reading, I hope this blog improved your understanding about how Room library works. If you would like to get in touch with me my email address is tanay1400089@gmail.com. LinkedIn. The link to the code used in this post Github Link.