I am trying to insert data from a prepopulated database (EnglishVocabs.db) into table "vocab" of my Android app (app_database.db). I am using the following code to perform this operation:
val appDbFile = context.getDatabasePath("app_database.db")
val appdb = SQLiteDatabase.openDatabase(appDbFile.path, null, SQLiteDatabase.OPEN_READWRITE)
val insertUniqueVocabsSqlForAppDb = """
ATTACH '${preDbFile.path}' AS preDb;
INSERT INTO vocab(word, language_id, parts_json)
SELECT DISTINCT B.word, ${Language.ENGLISH.id}, B.parts_json
FROM preDb.EnglishVocabs AS B
WHERE B.word NOT IN (SELECT A.word FROM vocab A);
""".trimIndent()
appdb.beginTransactionWithListener(object : SQLiteTransactionListener {
override fun onBegin() {
Logger.d("on begin")
}
override fun onCommit() {
Logger.d("on commit")
}
override fun onRollback() {
Logger.d("on rollback")
}
})
try {
Logger.d("attached db = ${appdb.attachedDbs}")
val c = appdb.rawQuery(insertUniqueVocabsSqlForAppDb, arrayOf())
appdb.setTransactionSuccessful()
Logger.d("transaction success")
if(c.moveToFirst()){
Logger.d("response = ${c.getStringOrNull(0)}")
}
c.close()
}catch (e: Exception){
Logger.e(e.stackTraceToString())
}finally {
appdb.endTransaction()
appdb.close()
}
I am able to successfully run this code and the onCommit() method of the transaction listener is being called, indicating that the transaction has been committed.
However, when I go to check the app_database.db, the data has not been inserted.
Interestingly, when I copy both the prepopulated and app databases to my PC and run the SQL code using SQLite DB Browser, the data is inserted successfully (40k rows in 200ms). I am not sure what the issue could be in the Android environment. I've grant all necessary permissions.
Can anyone help me understand why this might be happening and how I can fix it?
UPDATE:
I use sqldelight as my app database. and I tried sqlDriver.execute()... too, nothing works
The method rawQuery() is used to return rows and not for INSERT statements.
Instead you should use execSQL() in 2 separate calls:
appdb.execSQL("ATTACH '${preDbFile.path}' AS preDb");
val insertUniqueVocabsSqlForAppDb = """
INSERT INTO vocab(word, language_id, parts_json)
SELECT DISTINCT B.word, ${Language.ENGLISH.id}, B.parts_json
FROM preDb.EnglishVocabs AS B
WHERE B.word NOT IN (SELECT A.word FROM vocab A);
""".trimIndent()
appdb.execSQL(insertUniqueVocabsSqlForAppDb)
Related
Trying to understand and implement how Flow works under the hood with Room and SQLite.
For instance in Room, following code will allow to respond to data changes. Is there some kind of observable on the database that allow to propagate changes to the listeners.
fun getAll(): Flow<List<User>>
How can a Flow based approach be implemented to using plain vanilla approach using the following code.
fun getAll(): ArrayList<User>? {
val sql = "SELECT id, first, last FROM user"
var users: ArrayList<User>? = null
connection.readableDatabase.rawQuery(sql, null).use { cursor ->
if (cursor.count > 0) users = ArrayList()
while (cursor.moveToNext()) {
users?.add(User(cursor))
}
}
return users
}
I'm a beginner in asynchronous operations. My goal is to check whether data with specific date already exist in Room database or not, if not then start download from server and insert it to Room. But my following codes execute TODO: Processing Data in Fragment twice coz the coroutine re-execute it when the TODO: download insert new data finished
Here my codes:
birdDAO.kt
#Query("SELECT * FROM birds_table WHERE birdDate =:rDate ORDER BY birdId")
fun getBirdBySingleDate(rDate: Int): LiveData<List<Bird>>
birdRepository.kt
fun getBirdBySingleDate(rDate: Int) = birdDao.getBirdBySingleDate(rDate)
birdViewModel.kt
fun getBirdByDate(rDate: Int) = birdRepository.getBirdBySingleDate(rDate)
Fragment.kt
private fun loadBirdData(jDate: Int) {
val listBirdByDate = birdViewModel
.getBirdByDate(jDate)
.observe(viewLifecycleOwner){ birds ->
val size = birds.size
if(size > 0) {
//TODO Processing Data
}
else
{
//TODO: download n insert new data
}
}
}
The question is how to write the codes that only execute one-shot Room query? I tried to remove LiveData type in birdDAO.kt and change Fragment.kt like this:
private fun loadBirdData(jDate: Int) {
val listBirdByDate = birdViewModel.getBirdByDate(jDate)
if(listBirdByDate.isNotEmpty) {
//TODO Processing Data
}
else
{
//TODO: download n insert new data
}
}
but the listBirdByDate.isNotEmpty line gave me this error:
Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
public inline fun <T> Array<out TypeVariable(T)>.isNotEmpty(): Boolean defined in kotlin.collections
Or what is the best way to get my goal done? Thx
Instead of returning list of birds , you can create another query to get the count of found entries for particular date and in the view Model , I guess, you want to start downloading data from server once your verify that the count is zero.
You can change your viewModel from
fun getBirdByDate(rDate: Int) =
birdRepository.getBirdBySingleDate(rDate)
to
fun getBirdByDate(rDate :Int){
viewModelScope.launch {
var count= 2 //we don't want two in db though
//withContext and also with main dispatcher we assure that the count
//vairable gets updated , I recently came to know that we can actually
// use main as well immediate on Dispatcher inside viewmodel , so
//including it :P
//Thanks to #commonware
withContext(Dispatchers.Main.immediate){
count = //your repository function to get dao using Dispatche.IO
}
if(count == 0){
//start downloading from server and update data in db
}else if(count==1){
//get the list from db
}else
{
//show error
}
}
}
you can actually do better logic for if and else (use when for kotlin), I am trying to give you some idea as I am also new to android.
When updading DB is it acceptable to run large code to align the DB to my requirements.
For example, I need to alter the table and change column names. Then I need to get all my data in the DB and check if file is located than update the DB accordingly. I need it happen only once when user updates the app to this Room version.
val MIGRATION_8_9 = object : Migration(8, 9) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE RideEntity RENAME videoPresent TO videoState")
GlobalScope.launch(Dispatchers.IO) {
val rides = DataBaseHelper.getAllPartsFromDB() //get all data
rides.forEach {
val path = MyApp.appContext.getExternalFilesDir(null)!!.path + "/" + it.name + "/"
val file = File(path + VIDEO_FILE).exists()
if (file) {
it.videoState = 1
DataBaseHelper.updateData(it) //set the data
}
}
}
}
}
Where:
suspend fun getAllPartsFromDB() = withContext(Dispatchers.IO) {
val parts = db.rideDao().getAllParts()
parts
}
Function:
#Query("SELECT * FROM rideentity ORDER BY time DESC")
fun getAllParts(): List<Parts>
So my question, despite this works, is this way acceptable? And if the migrate function called only once when the app DB updated from version X to Y
Is it acceptable to manage large DB manipulations inside Room migration?
Yes. However you may wish to put the update loop inside a transaction.
And if the migrate function called only once when the app DB updated from version X to Y
Yes it is only called the one time. The Migration(8,9) determines this that is the Migration will only be invoked when the version, as stored in the database header, is 8 and then the version number is set to 9.
I'm trying to pull a row from a database using Room and LiveData.
The starting point for my code is:
networkScope.launch {
var asset = recordingAssetsViewModel.get(path)
runOnUiThread {
var dialogText = "Remote URL: ${asset?.value?.remotePath}\n"
dialog.setMessage(dialogText)
dialog.show()
}
}
When I watch in the debugger, path is /data/user/0/com.xxx.app/files/videos20547.306588963664-screencast.mp4.
The get function looks like this:
fun get(path: String) = recordingAssetRepository?.get(path)
That calls into this:
fun get(path: String): LiveData<RecordingAsset>? = recordingAssetDao?.findAssetByPath(path)
That function calls into this:
#Query("select * from video_table where path = :path limit 1")
fun findAssetByPath(path: String): LiveData<RecordingAsset>
As you can see from the image below, that path exists, and the remote_file field has text in it. But, I this "Remote URL: ${asset?.value?.remotePath}\n" always becomes "Remote URL: null\n" when I run it.
How do I troubleshoot where this is breaking in my app. Is there a way to turn on room debugging so I can see the SQL call it is making (or not making)?
You can't pull data like this from LiveData unless there's a data already. you should observe it using
//lifeCycleOwner in Fragment
//this in Activity
asset.observe(lifeCycleOwner) { value->
runOnUiThread {
var dialogText = "Remote URL: ${value?.remotePath}\n"
dialog.setMessage(dialogText)
dialog.show()
}
}
To get the result immediately just remove the LiveData wrapper.
#Query("select * from video_table where path = :path limit 1")
fun findAssetByPath(path: String): RecordingAsset
Other than that, just read the LogCat for errors in your sql or room misconfiguration :)
I'm trying to use requery https://github.com/requery/requery library with Kotlin and SQLite backend. I have a sql dump, which I want to write to sqlite database in the first launch of an application, and then I want to map data classes to database entities with requery.
Here is data source initialization with table creation:
if (!(DataStorage.isDbInitialized(context))) {
val db = writableDatabase
val inputStream = context?.resources?.openRawResource(R.raw.dump)
val reader = BufferedReader(InputStreamReader(inputStream))
val builder = StringBuilder()
var line : String?
var end = false
while (!end) {
line = reader.readLine()
if(line == null) {
end = true
} else {
builder.append(line)
}
}
db.execSQL(builder.toString())
onCreate(db)
DataStorage.setDbInitialized(context)
}
I have to derive this class from both SqlitexDatabaseSource and CommonDataSource to use with Kotlin. SQL query execuled successfully, but when I trying to select all objects from database, this request returns zero sized list:
val result : Result<Feat> = (application as MainApp).dataStore.select(Feat::class).get()
result.each {
Log.d("TAG", it.name)
}
DTO created as described in documentation:
https://github.com/Syjgin/PathfinderFeats/blob/master/app/src/main/java/com/syjgin/pathfinderfeats/model/Feat.kt
Is it possible to initialize requery data with sql dump, or I have to create DTO for each row and submit it via insert method?