Android Development
Room Database
Auto-generated ID
SQLite
Data Persistence

Android Room - Get the id of new inserted row with auto-generate

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Room can return the row id of an inserted entity when the primary key is marked as auto-generated. The reliable pattern is to use an auto-generated numeric primary key, return Long from the DAO insert method, and treat 0 as the "not yet assigned" value for new objects.

Define The Entity Correctly

For auto-generated ids, the entity usually looks like this:

kotlin
1import androidx.room.Entity
2import androidx.room.PrimaryKey
3
4@Entity(tableName = "notes")
5data class NoteEntity(
6    @PrimaryKey(autoGenerate = true)
7    val id: Long = 0L,
8    val title: String,
9    val body: String
10)

With Room's current behavior, if the primary key field is Long or Int, a value of 0 is treated as "not set" during insert when autoGenerate = true.

That is why new entities are usually created with id = 0L.

Return Long From The Insert Method

The DAO method can return the inserted row id directly:

kotlin
1import androidx.room.Dao
2import androidx.room.Insert
3
4@Dao
5interface NoteDao {
6    @Insert
7    suspend fun insert(note: NoteEntity): Long
8}

For collections, return a list or array of ids:

kotlin
@Insert
suspend fun insertAll(notes: List<NoteEntity>): List<Long>

This is the cleanest way to get the new id immediately after persistence.

Use The Returned Id In The Repository Layer

Capture the value where the insert happens:

kotlin
1class NoteRepository(private val noteDao: NoteDao) {
2    suspend fun create(title: String, body: String): Long {
3        val note = NoteEntity(
4            title = title,
5            body = body
6        )
7        return noteDao.insert(note)
8    }
9}

That makes follow-up operations such as navigation, detail loading, or child-record inserts straightforward and safe.

It is much better than inserting first and then querying "the most recent row," which is fragile under concurrency.

Use Transactions For Parent-Child Writes

If you need the new parent id to create related rows, keep the sequence inside a transaction:

kotlin
1data class NoteTagCrossRef(
2    val noteId: Long,
3    val tagId: Long
4)
5
6class NoteService(
7    private val noteDao: NoteDao,
8    private val crossRefDao: CrossRefDao
9) {
10    @androidx.room.Transaction
11    suspend fun createNoteWithTag(note: NoteEntity, tagId: Long) {
12        val noteId = noteDao.insert(note)
13        crossRefDao.insert(NoteTagCrossRef(noteId = noteId, tagId = tagId))
14    }
15}

This keeps the generated id and dependent writes in one consistent unit of work.

Conflict Strategy Affects The Returned Value

The insert result is clearer with OnConflictStrategy.ABORT, because a conflict becomes an error instead of an ambiguous insert outcome.

With IGNORE, Room can return -1 for rows that were skipped due to conflict. With REPLACE, Room will insert even on conflict, but the identity behavior may not match what callers assume.

That is why many applications use ABORT when the caller truly expects "create a brand new row and give me its id."

Test The Behavior Explicitly

A small Room test is worth having:

kotlin
1@Test
2fun insertReturnsPositiveId() = runTest {
3    val id = noteDao.insert(NoteEntity(title = "Draft", body = "Body"))
4    assertTrue(id > 0L)
5}

Tests are especially useful if your app depends on a specific conflict strategy or if you later refactor the entity schema.

Common Pitfalls

One common mistake is giving a nonzero id to a new entity even though auto-generation is enabled. That can change how Room interprets the insert.

Another issue is returning Unit from the DAO method and then trying to discover the inserted row by issuing a separate query afterward.

A third problem is ignoring conflict strategy semantics. IGNORE and REPLACE can produce results that are different from "always give me the new row id."

Finally, developers sometimes assume every insert API behaves identically for single entities and lists, but the return types differ for a reason.

Summary

  • Use @PrimaryKey(autoGenerate = true) with a numeric id type such as Long.
  • For new entities, pass 0L as the unset id value.
  • Return Long from a single-entity @Insert DAO method to get the generated row id.
  • Capture that id immediately instead of querying for "the last inserted row."
  • Be deliberate about conflict strategy because it affects insert semantics and returned ids.

Course illustration
Course illustration

All Rights Reserved.