Table of Contents
Why Data Access Objects Matter
In Room, a Data Access Object, or DAO, is the main place where you define how your app talks to the database. Entities describe what the tables look like, but DAOs describe what you can do with those tables. You do not write SQL everywhere in your app. Instead, you define a small set of clear operations inside DAO interfaces or abstract classes, and Room generates the implementation for you.
A DAO groups related database operations. For example, a UserDao groups all operations related to the User entity, such as inserting a user, finding a user by id, or listing all users. This keeps database code in one place and makes it easier to test and maintain.
In Room, every access to the database must go through a DAO. You do not call SQLite APIs directly when you use Room.
Defining a DAO in Room
A Room DAO is usually a Kotlin interface or abstract class annotated with @Dao. Room reads the annotations on the functions inside and generates a class with full database code.
A minimal DAO looks like this:
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): List<User>
}
Here the @Dao annotation tells Room that this interface is a Data Access Object. The type List<User> must match an entity type that Room knows, and the SQL query must be valid for the underlying SQLite table that corresponds to the User entity.
You normally define each DAO in its own file, named after the entity or feature, for example UserDao.kt for the User entity. The database class, which you will see in the Room database chapter, then exposes each DAO with an abstract function like fun userDao(): UserDao.
Basic Query Methods with @Query
The @Query annotation is the most flexible part of a DAO. With it, you can write SQL statements that read or modify data. For beginners, it is useful to start with simple SELECT, INSERT, UPDATE, and DELETE usage, and focus on how the function return type and parameters match the query.
A typical read query for all rows looks like this:
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAll(): List<User>
}
Room checks the SQL at compile time. The SELECT * FROM users statement must return columns that match the User entity fields.
You can filter by values with placeholders. Placeholders start with a colon and must match function parameters:
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
fun findById(userId: Long): User?
@Query("SELECT * FROM users WHERE name LIKE :name LIMIT 1")
fun findByName(name: String): User?
}
Here :userId and :name refer to the arguments of the DAO functions. Room binds the values correctly and protects you from SQL injection, as long as you use placeholders and do not build SQL strings manually.
You can also return collections or a single item, and you can use nullable types to indicate that a query might return no rows. If you declare a non-nullable type and the query returns no result, Room will throw an exception at runtime.
Insert Operations with @Insert
For inserting data, Room provides a special annotation @Insert. With @Insert, you do not write the SQL yourself. Room generates an INSERT statement based on the entity definition.
An insert method can insert a single entity:
@Dao
interface UserDao {
@Insert
fun insertUser(user: User)
}It can also insert multiple entities at once using a vararg parameter or a collection:
@Dao
interface UserDao {
@Insert
fun insertUsers(vararg users: User)
@Insert
fun insertAll(users: List<User>)
}
Often you want to know the row id that SQLite assigned to a new row. You can get this by using a Long return type for a single insert, or List<Long> for multiple inserts:
@Dao
interface UserDao {
@Insert
fun insertUser(user: User): Long
@Insert
fun insertUsers(users: List<User>): List<Long>
}
By default, if you try to insert a row that conflicts with an existing row (for example the same primary key), SQLite will throw an error. You can control this with onConflict:
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun upsertUser(user: User)
}
Here, OnConflictStrategy.REPLACE tells Room to replace the existing row if there is a conflict. Other strategies include ABORT, IGNORE, and ROLLBACK. Choose a strategy that makes sense for your data rules.
Update and Delete with @Update and @Delete
Room also provides simple annotations for updates and deletes. With @Update, Room generates SQL to update rows that match the primary key of the entity. With @Delete, it generates SQL to delete matching rows.
An update method might look like this:
@Dao
interface UserDao {
@Update
fun updateUser(user: User): Int
}
The return type Int tells you how many rows were updated. Room uses the primary key field in User to find which row in the table to update. It then sets all columns to match the fields in the entity object.
Deleting is similar:
@Dao
interface UserDao {
@Delete
fun deleteUser(user: User): Int
}You can pass a list or vararg of entities to update or delete multiple rows. Room will generate SQL that uses the primary keys of all entities you pass.
For more complex updates or deletes, such as changing only one column, you can use @Query with an explicit UPDATE or DELETE statement and define the return type as Int to get the affected rows count:
@Dao
interface UserDao {
@Query("UPDATE users SET isActive = 0 WHERE lastLogin < :timestamp")
fun deactivateOldUsers(timestamp: Long): Int
@Query("DELETE FROM users WHERE isActive = 0")
fun deleteInactiveUsers(): Int
}Return Types and LiveData or Flow
Room supports various return types in DAO methods. For synchronous access, you can use standard Kotlin types such as a single entity, a nullable entity, a List<Entity>, or a primitive count. For reactive or asynchronous access, Room also supports observable types like LiveData and Flow for queries.
A typical LiveData based query looks like this:
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun observeAll(): LiveData<List<User>>
}
When the data in the users table changes, the LiveData emits a new list automatically to any active observers. This works only for SELECT queries. Insert, update, and delete methods do not return LiveData or Flow.
For Kotlin coroutines, you can use Flow:
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE isActive = 1")
fun observeActiveUsers(): Flow<List<User>>
}
With Flow, your ViewModel can collect the data and update the UI whenever the database changes. You will see how LiveData and Flow integrate with ViewModel and lifecycle aware components in later chapters, but it is useful to know that DAO methods can directly return these types.
For observable queries, Room automatically re-runs the query and emits new values whenever any table in the query changes. You do not need to trigger refresh manually.
Suspending DAO Methods and Threading
Database operations can be slow, especially on large tables. You should not run heavy queries on the main thread. Room enforces this by default. To work well with coroutines, Room allows DAO methods to be suspend functions for long running work.
A typical suspend based DAO uses suspend on insert, update, delete, or select methods:
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUserById(id: Long): User?
@Insert
suspend fun insertUser(user: User): Long
@Update
suspend fun updateUser(user: User): Int
}You call these methods from a coroutine, usually in a ViewModel or a repository that uses a dispatcher suitable for I/O work.
Room allows main thread queries only if you explicitly enable it in the database builder. This is useful only for examples and tests. For real apps, use suspend functions or Flow to keep the main thread free.
Transactions and @Transaction
Sometimes you need to run several database operations together so that they either all succeed or all fail. This grouping is called a transaction. In Room, you can mark a DAO method with @Transaction. Room then wraps all the body of that method in a single transaction.
You can apply @Transaction to a function that calls other DAO methods:
@Dao
interface UserDao {
@Insert
suspend fun insertUser(user: User): Long
@Query("UPDATE stats SET userCount = userCount + 1")
suspend fun incrementUserCount()
@Transaction
suspend fun insertUserAndUpdateStats(user: User) {
insertUser(user)
incrementUserCount()
}
}
If any step inside insertUserAndUpdateStats fails, the entire transaction will be rolled back, so the database returns to the previous consistent state.
Transactions are also important when you read complex relationships between tables. Room uses @Transaction around certain queries that load related data so that you get a consistent snapshot. That more advanced usage appears when you work with relations, but the key idea remains the same: @Transaction groups operations into a single atomic unit.
DAO Design and Separation of Concerns
Each DAO should represent a clear responsibility in your app. A common pattern is to have one DAO per entity. For example, UserDao for users, PostDao for posts, and CommentDao for comments. If the app grows, you might also group by feature, such as FeedDao for feed related queries that touch multiple tables.
Try to avoid putting too much logic inside DAO methods. DAOs should focus on translating between function calls and SQL operations. Higher level logic, such as combining data from network and database, usually belongs in repositories or use case classes, which call DAO methods.
This separation keeps DAOs simple to test, since you can check that each function reads or writes the correct data. It also makes changes to your database schema easier, since all SQL is concentrated in small, predictable places.
Testing and Using DAOs
You interact with DAOs through the Room database class. After you create a RoomDatabase subclass, you get DAO instances from it and call the methods you defined in your DAOs.
In tests, you can build an in memory database and run DAO methods directly to verify behavior. Since DAOs are just interfaces or abstract classes, Room provides the full implementation at compile time, and you can use them as normal Kotlin objects.
By keeping your data access logic inside DAOs, you avoid scattered SQL, reduce mistakes, and get compile time checks from Room. As you continue through the Room chapters, the DAO will remain the central place where your database interactions live, while entities define the structure and migrations handle schema evolution.