Introduction to Data Access Objects

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

This section covers how to create DAOs for your Room database.

Data Access Objects (DAOs) are interfaces that provide methods for accessing the database. You annotate the interface with @Dao annotation to inform Room that the interface is a DAO object. You define functions in your DAO to perform operations such as inserting, reading, updating, and deleting data. In Room, DAOs abstract the database operations and provide a clean API for the rest of the app to interact with the database.

An example of a DAO interface is shown below:

@Dao
interface NotesDao {

}

The functions that you define in your DAO can either be:

  • Convenience functions: With these functions, Room automatically generates the SQL queries for you. You annotate these functions with either @Insert, @Update, or @Delete annotations.
  • Query functions: These are functions where you define the SQL query yourself using the @Query annotation. Room will validate the query at compile time and provide you with a compile-time error if the query is incorrect.

You’ll cover how to define these functions in the next sections.

To insert data into the database, you define a function in the DAO interface and annotate it with @Insert annotation as shown below:

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(note: NoteEntity)

The above function inserts a note into the database. The @Insert annotation automatically generates the SQL query to insert the note into the database. You also add the onConflict attribute that specifies the conflict resolution strategy. In this case, you set it to OnConflictStrategy.REPLACE, which means that if there’s a conflict, the existing note will be replaced with the new note. You mark the function as suspend because Room doesn’t support main thread database operations. You’ll need to use coroutines to perform the operation in a background thread.

Using the @Insert annotation, you can also insert multiple notes into the database, as shown below:

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(notes: List<NoteEntity>)

The @Insert annotation can return a Long value. The value represents the rowId of the inserted item when you pass a single entity instance. Or, it will represent an array of long values of rowIds when you pass an array or collection of entities as the parameter in your insert function. You can use this value to determine if the insert operation was successful.

When reading data from the database, you also need to do it off the main thread. Room provides support for asynchronous queries using either LiveData, RxJava, or Kotlin’s Flow. In this lesson, you’ll use Kotlin’s Flow to read data from the database. One thing to note is that when your return type is a Flow, you don’t need to use the suspend keyword in the function signature. This is because Flow is already asynchronous.

To read the data you’ve inserted into the database, you define a function in the DAO interface and annotate it with @Query annotation as shown below:

@Query("SELECT * FROM Notes")
fun getNotes(): Flow<List<NoteEntity>>

You annotate the function with @Query, and inside the annotation, you specify the SQL query to retrieve all notes from the database. In this case, you define a simple SELECT query to retrieve all notes from the Notes table. The function returns a Flow of List<NoteEntity>, which means that the function will return a list of notes as a stream of data that can be observed.

At times, you need to retrieve a single note from the database. You can do this by adding a parameter to the @Query annotation as shown below:

@Query("SELECT * FROM Notes WHERE id = :id")
fun getNoteById(id: Int): Flow<NoteEntity>

In the example above, the @Query annotation creates the SQL query to retrieve a note with the specified ID from the Notes table. The function returns a Flow of NoteEntity, which means that the method will return a single note as a stream of data that can be observed.

Room also provides the @Transaction annotation that you can use to run multiple queries in a single transaction. This is useful when you need to perform multiple database operations. It ensures that all operations are successful before committing the transaction. You can use the @Transaction annotation as shown below:

@Transaction
suspend fun insertAndDelete(note: NoteEntity) {
  insert(newNote)
  delete(oldNote)
}

In the example above, you define a function that inserts a new note into the database and then deletes the old note. You annotate the function with the @Transaction annotation to tell Room that this function should be run as a single transaction. This means that if either the insert or delete operation fails, the entire transaction will be rolled back. In this case, the database will remain unchanged.

To update a note in the database, you define a function in the DAO interface and annotate it with @Update annotation as shown below:

@Update
  suspend fun update(note: NoteEntity)

In the above example, the @Update annotation tells Room that the function is an update operation. Room will find the note with the same id as the note you’re updating and update it with the new note data.

To delete a note from the database, you can do it in two ways, as shown below:

@Delete
suspend fun delete(note: NoteEntity)

@Query("DELETE FROM Notes")
suspend fun deleteAllNotes()

To explain the two functions above:

  • delete(note: NoteEntity): You annotate this function with the @Delete annotation. Room will find the note with the same id as the note you’re deleting and delete it from the database.
  • deleteAllNotes(): In this function, you use the @Query annotation to create an SQL query to delete all notes from the Notes table.

Depending on your use case, you can choose to delete using the @Delete annotation or the @Query annotation.

Out of the box, Room only supports storing primitive data types such as String, Int, Long, Boolean, and Float. You can store custom objects using the @Embedded annotation. But at times, you might need to store custom data types like collections. In such cases, you can use TypeConverters to convert the custom data types to a format that Room can store in the database. You create TypeConverters using the @TypeConverter annotation. An example of a TypeConverter for a collection of items is shown below:

class DevScribeTypeConverters {
  @TypeConverter
  fun fromList(list: List<String>): String {
    return list.joinToString(",")
  }

  @TypeConverter
  fun toList(data: String): List<String> {
    return data.split(",").map { it }
  }
}

In the example above, you create a TypeConverter for a list of strings. The fromList function converts a list of strings to a single string by joining them with a comma. The toList function converts the single string back to a list of strings by splitting them using a comma. Each function is annotated with the @TypeConverter annotation to tell Room that this function is a TypeConverter.

After creating the TypeConverter class, you need to add it to the Room database class. Use the @TypeConverters annotation as shown below:

@Database(entities = [NoteEntity::class], version = 1)
@TypeConverters(DevScribeTypeConverters::class)
abstract class NotesDatabase : RoomDatabase() {
  abstract fun notesDao(): NotesDao
}

Now, in to your entity class, you can use the custom data type as shown below:

@Entity(tableName = "Notes")
data class NoteEntity(
  @PrimaryKey(autoGenerate = true)
  val id: Int = 0,
  val title: String,
  val content: String,
  val tags: List<String>
)

In the NoteEntity class, you have a tags property, which is a list of strings. You can now store the list of strings in the database using the TypeConverter you created. Additionally, you can also query the database and retrieve the list tags as a list of strings:

@Query("SELECT tags FROM Notes WHERE id = :id")
fun getTagsById(id: Int): Flow<List<String>>

Without the TypeConverter, you wouldn’t be able to store the list of strings in the database, and Room would throw a compile time error. It wouldn’t know how to store the list of strings.

See forum comments
Download course materials from Github
Previous: Demo: Creating Entities Next: Demo: Creating DAOs