Search code examples
androidandroid-room

(Anrdoid) How to prepopulate the Room database


enter image description here

I made a screen like the current image.

Data such as A, B, C.. are currently being set by getting from the strings.xml resource file.

I am now going to use Room DB instead of strings.xml and I want to get these data from Room.

To do this, we need to pre-populate the Room with data.

In the sample code I found, the method called addCallback() was usually used.

like this :

@Database(entities = arrayOf(Data::class), version = 1)
abstract class DataDatabase : RoomDatabase() {

    abstract fun dataDao(): DataDao

    companion object {

        @Volatile private var INSTANCE: DataDatabase? = null

        fun getInstance(context: Context): DataDatabase =
                INSTANCE ?: synchronized(this) {
                    INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
                }

        private fun buildDatabase(context: Context) =
                Room.databaseBuilder(context.applicationContext,
                        DataDatabase::class.java, "Sample.db")
                        // prepopulate the database after onCreate was called
                        .addCallback(object : Callback() {
                            override fun onCreate(db: SupportSQLiteDatabase) {
                                super.onCreate(db)
                                // insert the data on the IO Thread
                                ioThread {
                                    getInstance(context).dataDao().insertData(PREPOPULATE_DATA)
                                }
                            }
                        })
                        .build()

        val PREPOPULATE_DATA = listOf(Data("1", "val"), Data("2", "val 2"))
    }
}

However, as you can see from the code, in the end, data (here, val PREPOPULATE_DATA) is being created again within the code. (In another code, db.execSQL() is used)

In this way, there is no difference from fetching data from resource file in the end.

Is there any good way?

Developer documentation uses assets and files.

However, it is said that it is not supported within In-memory Room databases.

In this case, I do not know what In-memory means, so I am not using it.


Solution

  • In this case, I do not know what In-memory means, so I am not using it.

    In-Memory will be a database that is not persistent, that is the database is created using in memory rather than as a file, at some time it will be deleted. You probably do not want an in-memory database.

    However, as you can see from the code, in the end, data (here, val PREPOPULATE_DATA) is being created again within the code. (In another code, db.execSQL() is used)

    This is a common misconception when writing Apps as the onCreate method of an activity is often repeated when an App is running. With an SQLite database the database is created once in it's lifetime, which would be from the very first time the App is run until the database file is deleted. The database will otherwise remain (even between App version changes).

    Is there any good way?

    You basically have two options for a pre-populated database. They are

    1. to add the data when/after the database is created, as in your example code (which is not a good example as explained below), or
    2. to utilise a pre-packaged database, that is a database that is created outside of the App (typically using an SQlite tool such as DBeaver, Navicat for SQlite, SQLiteStudio, DB Browser for SQLite).

    Option 1 -Adding data

    If the data should only be added once then using the overridden onCreate method via the CallBack can be used. However, using functions/methods from the @Dao annotated class(es) should not be used. Instead only SupportSQLiteDatabase functions/methods should be used e.g. execSQL (hence why the SupportSQLiteDatabase is passed to onCreate).

    This is because at that stage the database has just been created and all the underlying processing has not been completed.

    You could protect against duplicating data quite easily by using INSERT OR IGNORE .... rather than INSERT ..... This will skip insertion if there is an applicable constraint violation (rule being broken). As such it relies upon such rules being in force.

    The two most commonly used constraints are NOT NULL and UNIQUE, the latter implicitly for a primary key.

    • In your case if a Data object has just the 2 fields (columns in Database terminology) then, as Room requires a primary key, an implicit UNIQUE constraint applies (could be either column or a composite primary key across both). As such adding Data(1,"val") a second time would result in a constraint violation which would result in either
    1. The row being deleted and another inserted (if INSERT OR REPLACE)
      1. This further complicated by the value of autogenerate.
    2. An exception due to the violation.
    3. The insert being skipped if INSERT OR IGNORE were used.

    This option could be suitable for a small amount of data but if over used can start to bloat the code and result in it's maintainability being compromised.

    • If INSERT or IGNORE were utilised (or alternative checks) then this could, at some additional overhead, even be undertaken in the Callback's onOpen method. This being called every time the database is opened.

    Pre-packaged Database

    If you have lots of initial data, then creating the database externally, including it as an asset (so it is part of the package that is deployed) and then using Room's .createFromAsset (or the rarer used .createFromFile) would be the way to go.

    However, the downfall with this, is that Room expects such a database to comply with the schema that it determines and those expectations are very strict. As such just putting together a database without understanding the nuances of Room then it can be a nightmare.

    • e.g. SQLite's flexibility allows column types to be virtually anything (see How flexible/restricive are SQLite column types?). Room only allows column types of INTEGER, TEXT, REAL or BLOB. Anything else and the result is an exception with the Expected .... Found ... message.

    However, the easy way around this is to let Room tell you what the schema it expects is. To do so you create the @Entity annotated classes (the tables), create the @Database annotated class, including the respective entities in the entities parameter and then compile. In Android Studio's Android View java(generated) will then be visible in the explorer. Within that there will be a class that is the same name as the @Database annotated class but suffixed with _Impl. Within this class there is a function/method createAllTables and it includes execSQL statements for all the tables (the room_master_table should be ignored as Room will always create that itself).

    The database, once created and saved, should be copied into the assets folder and using .createFromAsset(????) will then result in the pre-packaged data being from the package to the appropriate local storage location.