Search code examples
androidandroid-room

Room onCreate callback java.nio.channels.OverlappingFileLockException


I'm using Room onCreate() callback to initialize some data when the db is created. Using hilt:

@Singleton
@Provides
fun provideDatabase(@ApplicationContext applicationContext: Context): AppDatabase {
    return Room.databaseBuilder(applicationContext, AppDatabase::class.java, DATABASE_NAME)
        .allowMainThreadQueries()
        .fallbackToDestructiveMigration()
        .addCallback(object : RoomDatabase.Callback() {
            override fun onCreate(db: SupportSQLiteDatabase) {
                super.onCreate(db)
                val provideDatabase = provideDatabase(applicationContext)
                provideDatabase.userDao().createUser(LocalUser())
            }
        })
        .build()
}

But I got exception when using userDao:

java.nio.channels.OverlappingFileLockException

Not sure why it happen, maybe I'm using Room+Hilt wrongly?

Based on this tutorial without hilt it should work without exception: https://developermemos.com/posts/prepopulate-android-room-data#google_vignette


Solution

  • When the onCreate callback is invoked, the database has already been created. Calling onCreate again as per super.onCreate will try to again create the database, the exact same file and hence encounter the lock.

    The database is passed to the callback hence db: SupportSQLiteDatabase

    Consider, as an example:-

                    ....
                    .addCallback(object : RoomDatabase.Callback() {
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            //super.onCreate(db)
                            //val provideDatabase = provideDatabase(applicationContext)
                            //provideDatabase.userDao().createUser(LocalUser())
                            DatabaseUtils.dumpCursor(db.query("SELECT * FROM sqlite_master"))
                        }
                    })
                    .build()
    

    Where the schema (sqlite_master) is queried and the resultant cursor dumped (output to the log). e.g. using the above results in:-

    2024-04-17 17:33:55.152 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@68ac7e0
    2024-04-17 17:33:55.152 I/System.out: 0 {
    2024-04-17 17:33:55.152 I/System.out:    type=table
    2024-04-17 17:33:55.152 I/System.out:    name=android_metadata
    2024-04-17 17:33:55.152 I/System.out:    tbl_name=android_metadata
    2024-04-17 17:33:55.152 I/System.out:    rootpage=3
    2024-04-17 17:33:55.152 I/System.out:    sql=CREATE TABLE android_metadata (locale TEXT)
    2024-04-17 17:33:55.153 I/System.out: }
    2024-04-17 17:33:55.153 I/System.out: 1 {
    2024-04-17 17:33:55.153 I/System.out:    type=table
    2024-04-17 17:33:55.153 I/System.out:    name=tasks
    2024-04-17 17:33:55.153 I/System.out:    tbl_name=tasks
    2024-04-17 17:33:55.153 I/System.out:    rootpage=4
    2024-04-17 17:33:55.153 I/System.out:    sql=CREATE TABLE `tasks` (`taskId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)
    2024-04-17 17:33:55.153 I/System.out: }
    2024-04-17 17:33:55.153 I/System.out: 2 {
    2024-04-17 17:33:55.153 I/System.out:    type=table
    2024-04-17 17:33:55.153 I/System.out:    name=sqlite_sequence
    2024-04-17 17:33:55.153 I/System.out:    tbl_name=sqlite_sequence
    2024-04-17 17:33:55.153 I/System.out:    rootpage=5
    2024-04-17 17:33:55.153 I/System.out:    sql=CREATE TABLE sqlite_sequence(name,seq)
    2024-04-17 17:33:55.153 I/System.out: }
    2024-04-17 17:33:55.153 I/System.out: 3 {
    2024-04-17 17:33:55.153 I/System.out:    type=table
    2024-04-17 17:33:55.153 I/System.out:    name=TaskTaskCR
    2024-04-17 17:33:55.153 I/System.out:    tbl_name=TaskTaskCR
    2024-04-17 17:33:55.153 I/System.out:    rootpage=6
    2024-04-17 17:33:55.153 I/System.out:    sql=CREATE TABLE `TaskTaskCR` (`parentTaskId` INTEGER NOT NULL, `childTaskId` INTEGER NOT NULL, PRIMARY KEY(`parentTaskId`, `childTaskId`))
    2024-04-17 17:33:55.153 I/System.out: }
    2024-04-17 17:33:55.153 I/System.out: 4 {
    2024-04-17 17:33:55.153 I/System.out:    type=index
    2024-04-17 17:33:55.153 I/System.out:    name=sqlite_autoindex_TaskTaskCR_1
    2024-04-17 17:33:55.153 I/System.out:    tbl_name=TaskTaskCR
    2024-04-17 17:33:55.153 I/System.out:    rootpage=7
    2024-04-17 17:33:55.154 I/System.out:    sql=null
    2024-04-17 17:33:55.154 I/System.out: }
    2024-04-17 17:33:55.154 I/System.out: 5 {
    2024-04-17 17:33:55.154 I/System.out:    type=table
    2024-04-17 17:33:55.154 I/System.out:    name=room_master_table
    2024-04-17 17:33:55.154 I/System.out:    tbl_name=room_master_table
    2024-04-17 17:33:55.154 I/System.out:    rootpage=8
    2024-04-17 17:33:55.154 I/System.out:    sql=CREATE TABLE room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)
    2024-04-17 17:33:55.154 I/System.out: }
    2024-04-17 17:33:55.154 I/System.out: <<<<<
    

    i.e. as can be seen the database consists of the following:-

    • the table android_metadata* (an Android specific system table that contains the locale)
    • the table tasks (an application table as per an @Entity annotated class)
    • the table sqlite_sequence (an SQLite system table, due to AUTOINCREMENT being used)
    • the table TaskTaskCR (an application table as per an @Entity annotated class)
    • the index sqlite_autoindex_TaskTaskCR_1 (automatically generated by SQLite)
    • the table room_master (a Room table that contains a hash of the schema used for checking to see if the schema has been changed)

    What you should be doing is utilising the passed SupportSQLiteDatabase to initialise the data. That's why it is passed to the callback.

    e.g. db.execSQL("INSERT INTO tasks (name) VALUES('Task001')")

    • obviously this suits the database used for this answer and is very likely not what you would use.

    Using the above with the extra line, then App Inspection, after running, shows:-

    enter image description here

    • i.e. the row has been added
      • the other rows are added later, as the demo used otherwise unchanged code from a previous answer.