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:
-
@Insert
for inserts -
@Update
for updates -
@Delete
for deletions -
@Query
for anything, but mostly used for data retrieval
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:
- 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. - So long as we have 1+ observers of the
Flow
, if we do other database operations that affect thetodos
table, Room will automatically deliver a new result to those observers via theFlow
. 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.