I now finished the android codelab about room db with MVVM arch. But there is one part that i didn't exactly understand. This is a sentence from the codelab:
To delete all content and repopulate the database
whenever the app is created, you'll create a RoomDatabase.Callback and override onCreate().
and this is the code they provide:
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.wordDao())
}
}
}
suspend fun populateDatabase(wordDao: WordDao) {
// Delete all content here.
wordDao.deleteAll()
}
}
the part that I don't understand is "deleting all content". why do I need to delete all content when the app is created? and what do they mean by "whenever the app is created"? is it for the first time the app installed or everytime app is opened?
when I don't use this code, the app works fine too. can someone explain the purpose of deleting everything?
why do I need to delete all content when the app is created?
You don't need to, this is a specific scenario where for whatever reason you want the App to delete all content stored in the database. However there should not be any anyway as onCreate is only called when the database doesn't actually exist (unless manually invoked), so there will be no content to delete.
If you used a pre-packaged database using the .createFromAsset then onCreate isn't called.
I believe that they have just included a simple, unlikely to fail, introduction to using a Callback.
and what do they mean by "whenever the app is created"? is it for the first time the app installed or everytime app is opened?
The former. That is the whole purpose of a Database is to store data long term. As such the database is stored in the App's data space. When an App is installed then the data space will not have the database. So the App and thus Room has to know to create the database before it can be used.
So when you attempt to use the database, the processing of preparing to use the database checks to see if the database exists.
If the database does exist then it carries on without calling onCreate.
If the database does not exist then Room will try to create the database and the tables therein via the onCreate method. The Callback allows intervention at this stage by overriding the onCreate method.
If the App is stopped and rerun the database will still exist, and onCreate is not called.
If the App is uninstalled and then re-installed the the database will be deleted, so onCreate is called.
If a new version of the App is installed, then the database will still exist and onCreate is not called.
If a new version of the App is installed and it includes a new version for the database, onCreate is not called. Instead whatever migration path is specified will be taken.
Related
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.
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
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
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
The row being deleted and another inserted (if INSERT OR REPLACE)
This further complicated by the value of autogenerate.
An exception due to the violation.
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.
In the app I'm working on, we had a complex manual migration that required data parsing, manual SQL commands, etc. This was to convert a List<X> column into a new linked table of X. I've previously written about the approach, but the specific commands are not especially relevant for this question.
The issue I'm encountering is ~1% of users are experiencing a crash as part of this migration. This cannot be reproduced in testing, and due to our table's size, Crashlytics cannot show any useful error:
Losing customer data isn't catastrophic in this context, but being stuck in the current "try migrate, crash, reopen app and repeat" loop is. As such, I want to just give up on the migration and fall back to a destructive migration if we encounter an exception.
Any ideas how this can be done? My current solution is rerunning the DB changes (but not the presumably failing data migration) inside the catch, but this feels very hacky.
Our database is defined as:
Room.databaseBuilder(
context.applicationContext,
CreationDatabase::class.java,
"creation_database"
)
.addMigrations(MIGRATION_11_12, MIGRATION_14_15)
.fallbackToDestructiveMigration()
.build()
where MIGRATION_14_15 is:
private val MIGRATION_14_15 = object : Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) {
try {
// database.execSQL create table etc
} catch (e: Exception) {
e.printStackTrace()
// Here is where I want to give up, and start the DB from scratch
}
}
}
The problem you have is that you cannot (at least easily) invoke the fall-back as that is only invoked when there is no migration.
What you could do is to mimic what fall back does (well close to what it does). That is the fall-back will delete (I think) the database file and create the database from scratch and then invoke the databases _Impl (generated java) createAllTables method.
However, you would likely have issues if you deleted the file as the database connection has been passed to the migration.
So instead you could DROP all the app's tables using the code copied from the dropAllTables method from the generated java. You could then follow this with the code from the createAllTables method.
These methods are in the generated java as the class that is the same as the class that is annotated with #Database suffixed with _Impl.
The gotcha, is that the exception
(Expected .... Found ....) that you have shown is NOT within the migration but after the migration when Room is trying to build the database, so you have no control/place to do the above fall-back mimic unless this was done for all 14-15 migrations.
Perhaps what you could do is to trap the exception, present a dialog requesting the user to uninstall the app and to then re-install. This would then bypass the migration as it would be a fresh install.
I have an application with prefilled database (I use Room, Kotlin). Database class:
#Database(
entities = [FurnitureModel::class, ImageModel::class],
version = Database.VERSION,
exportSchema = true
)
abstract class Database : RoomDatabase() {
abstract val furnitureDao: FurnitureDao
abstract val imageDao: ImageDao
companion object {
const val NAME = "application-data"
const val VERSION = 3
#Volatile
private var instance: Database? = null
fun getInstance(context: Context): Database {
synchronized(this) {
var inst = instance
if (inst == null) {
inst = Room.databaseBuilder(
context.applicationContext,
Database::class.java,
"$NAME-local.db"
)
.fallbackToDestructiveMigration()
.createFromAsset("$NAME.db")
.build()
instance = inst
}
return inst
}
}
}
}
It worked fine. Once I decided to update some data inside database. No stucture changes, just adding and changing some data. For versions, I changed constant VERSION from 3 to 4. When I run the application, I've got empty database. As I understand, it is possible to get empty database with fallbackToDestructiveMigration in few situations. I could made some mistake somewhere in data. To get more details I decided to run application without preinstalled previous version. Clean install, if I can say like this. To do it, I wiped data for Virtual device in AVD Manager, removed build directories in application and app directories and run 'Invalidate cache/restart...' in Android Studio (I know that there is no need to do it). But the result is always the same: trying to install my application I always get error
Caused by: java.lang.IllegalStateException: A migration from 3 to 4 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.
As I understand, the error must not be shown when you install the application for the first time. But it is shown. I tried to create new virtual device and run app there, but the error is still there.
What should I do to forse Android Studio to forget that the application was installed before?
Thank you.
UPD1: I don't need to keep any data, actually I want to delete all data from previous versions of application. But after wiping data it still works as if I didn't wipe it. This is the problem.
RESULT: I found that the problem was in prefilled database. Unfortunatedly I can't find that is exactly wrong with database. I disabled creating database from assets, run application so that Room created bew empty database, copied the database from emulator and compared each table with my database. No any difference. So to solve the problem I just moved all data in each table to the same table in new empty database (using DataGrid). After running the application new database the problem disapeared.
So currently I have a Dao with a function that emits a Flow<>
#Query("SELECT * FROM ${Constants.Redacted}")
fun loadAllContacts(): Flow<List<Redacted>>
I am calling this from a repository like so
val loadAllContacts: Flow<List<Redacted>> = contactDao.loadAllContacts()
I am injecting the repository into the viewModel's constructor, and then at the top of my viewModel I have a val like so
val contacts: LiveData<List<Redacted>> = contactRepository.loadAllContacts.asLiveData()
Which is being observed in my Activity like so
viewModel.contacts.observe(this) { contacts ->
viewModel.onContactsChange(contacts)
}
My thinking is that the Flow is converted to a LiveData, and then I can observe this LiveData from my activity and kick off this function to actually update the viewModel upon the data being updated.
For now onContactsChange just looks like
fun onContactsChange(list: List<Redacted>) {
Timber.i("VIEW UPDATE")
}
The problem is that I only see this Timber log upon opening the activity, and never again. I verified that data IS going into my database, and I verified that an insert occurred successfully while the activity & viewModel are open. But I never see the log from onContactsChange again. When I close the activity, and reopen it, I do see my new data, so that is another reason I know my insert is working correctly.
I would like to add that I am using a single instance (singleton) of my repository, and I think I can verify this by the fact that I can see my data at all, at least when the view is first made.
Figured it out:
Note: If your app runs in a single process, you should follow the singleton design pattern when instantiating an AppDatabase object. Each RoomDatabase instance is fairly expensive, and you rarely need access to multiple instances within a single process.
If your app runs in multiple processes, include enableMultiInstanceInvalidation() in your database builder invocation. That way, when you have an instance of AppDatabase in each process, you can invalidate the shared database file in one process, and this invalidation automatically propagates to the instances of AppDatabase within other processes.
It's a little bit hard to follow your question, but I think I see the overall problem with your Flow object not updating the way you want it too.
Following this quick tutorial, it seems that first you should declare your Flow object inside your Repository the same way you're already doing
val loadAllContacts: Flow<List<Redacted>> = contactDao.loadAllContacts()
and have your VM 'subscribe' to it by using the collect coroutine which would then allow you to dump all this data into a MutableLiveData State
data class YourState(..)
val state = MutableLiveData<YourState>()
init {
contactRepository.loadAllContacts().collect {
if (it.isNotEmpty()) {
state.postValue(YourState(
...
)
}
}
}
that your Activity/Fragment could then observe for changes
viewModel.state.observe(.. { state ->
// DO SOMETHING
})
P.S. The tutorial also mentions that because of how Dao's work, you might be getting updates for even the slightest of changes, but that you can use the distinctUntilChanged() Flow extension function to get more specific results.
I have two activities. First activity shows list of notes. Notes themselves are lists.
I use Android Architecture Components: ViewModel, LiveData; with Repository, Room, Dao, etc.
So, I make a method getAllNotes() in Dao, Repository and ViewModel like in google sample apps. In onCreate method of first activity I call observe and set adapter's content of a RecyclerView. And it works fine - it shows the list with Note titles.
Like that:
override fun onCreate(savedInstanceState: Bundle?) {
//some code
viewModel = obtainViewModel()
viewModel.getAllNotes().observe(this, Observer<List<Notes>> { notes ->
recView.setNote(notes)
}
}
Then I have a button that starts new Activity to create new Note. That note contains list of Lines which for now contains only string and foreign key.
data class Line {
var id: Long? = null
var note_id: Long? = null
var payload: String? = null
}
Note and Line are one-to-many relation and they are connected by id of Note and foreign key note_id in Line.
(I don't write here all of the code, it works, trust me)
The problem is, that to insert Lines in database I firstly need to insert the parent Note and I do that. And it works almost OK too. But the liveData of the getAllNotes() from the first Activity gets notified by this insertion. And if the user, as a result, decides to delete all the lines and go back to the first activity even if I delete temporary Note entity from the database the list on the first Activity shows it for a moment because it gets deleted in a background with a small delay.
What I see as a solution:
1) Unsubscribe observers from livedata. I tried to do it in onStop method, but it gets called after the onCreate method of the second activity where the entity is being created, so the livedata already gets notified and observers are removed after temporary Note passed into the list.
2) Not use Room/SQLite as cache. Since this Note and Lines are not guaranteed to stay then and shouldn't be shown or inserted into a table. So, I can keep it all in properties of viewModel (i.e. in memory). But I see a lot of overhead work to save these entities through screen rotation, minimizing the app and all that stuff with saving state and restoring it.
3) Create two additional entities like CachedNote and CachedLine and corresponding tables, to work with it until I decide to persist the work, insert it into original tables and show it.
4) Add property to the Note entity like "visible" and add this parameter to Query, to make entity note shown, until I decide to persist the work. But there could be a lot of "updateNoteWithLines" every where.
What should I do? I didn't google anything useful.
I know it's like "What's the best way question", forgive me.
You can try to call the observe in onResume and then call removeObserver in onPause, that way the Activity will not be updated, please look at the example here.