Step #3: Crafting a DAO

The @Entity class says “this is what my table should look like”. A @Dao class says “this is how I want to read and write from that table”. With Room, we define an interface or abstract class to describe the API that we want to have for working with the database. Room then code-generates an implementation for us, dealing with all of the SQLite code for getting our entities to and from our table.

Inside the ToDoEntity class (i.e., inside a {} that you add after the constructor), add this nested interface:

  @Dao
  interface Store {
    @Query("SELECT * FROM todos ORDER BY description")
    fun all(): Flow<List<ToDoEntity>>

    @Query("SELECT * FROM todos WHERE id = :modelId")
    fun find(modelId: String?): Flow<ToDoEntity?>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun save(vararg entities: ToDoEntity)

    @Delete
    suspend fun delete(vararg entities: ToDoEntity)
  }

The @Dao annotation tells Room that this interface serves as a DAO and defines an API that we want to use. On it, we have four functions. Each has an annotation indicating what is the database operation that this method should apply:

The @Query annotations always take a SQL statement as an annotation property, to indicate what SQL should be executed when this function is called. That SQL statement sometimes stands alone, as does SELECT * FROM todos for the all() function. However, the SQL can reference function parameters, such as in the case of the find() function. It has a modelId parameter, and our SQL statement refers to that, using a : prefix to identify that it is a reference to a parameter name (SELECT * FROM todos WHERE id = :modelId).

Query functions based on SELECT statements return whatever it is that the query is supposed to return. In our case, we are querying all columns from the todos table, and we are asking Room to map those rows to instances of our ToDoEntity class. For the all() function, we are expecting that there may be more than one, so the return type is based on a List of entities. By contrast, find() expects at most one result, so the return type is based on a single ToDoEntity.

We could have written all() and find() like this:

    @Query("SELECT * FROM todos")
    fun all(): List<ToDoEntity>

    @Query("SELECT * FROM todos WHERE id = :modelId")
    fun find(modelId: String): ToDoEntity

In that case, those functions would be synchronous, blocking until the query is complete.

Instead, our functions wrap our desired return values in Flow, from Kotlin’s coroutines system. This has two key effects:

  1. Room will perform the queries on a background thread and post the results to the Flow when the results are ready. Hence, our functions are asynchronous, returning immediately, rather than blocking waiting for the database I/O to complete.
  2. So long as we have 1+ observers of the Flow, if we do other database operations that affect the todos table, Room will automatically deliver a new result to those observers via the Flow. So, if we insert or delete a row from our table, observers will get updated data, which (if appropriate) will reflect those data changes.

Our other two functions — save() and delete() — use other Room annotations. save() uses @Insert, while delete() uses @Delete.

We are using save() for both inserts and updates. The onConflict = OnConflictStrategy.REPLACE property in our @Insert annotation says “if there already is a row with this primary key in the database, replace it with new contents”. So, if we pass in a brand-new ToDoEntity, it will be inserted, but if we pass in a ToDoEntity that reflects a change to an existing row, that row will be updated.

Note that both save() and delete() use vararg. This allows us to pass as many entities as we want, with all of them being saved or deleted. This is not required — you can have @Insert or @Delete functions that accept a single entity, a List of entities, etc.

And, note that both save() and delete() are suspend functions. As with Flow, suspend comes from Kotlin coroutines. Room will have save() and delete() perform their I/O on background threads, but from a programming standpoint, it will feel like we are making synchronous calls on the current thread.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.