I am using Room Database from the new Architecture components in my project.
I am adding some data through dao, but when trying to retrieve it I am not getting it. Can you please suggest me how to check whether the insert was successful or not? Below are the codes to help you understand the problem.
adding to database, I checked with debugger, this statement executed successfully.
appContext.db.rallyDAO().addVehicleListItem(vehicle)
Getting null from database on this statement after insert.
val v = appContext.db.rallyDAO().getVehicleListItem(it.vehicleID)
RoomDatabase
#Database(entities = arrayOf(Rally::class, Route::class, CheckPoints::class, Vehicles::class, VehicleListItem::class), version = 1)
abstract class TSDRoom: RoomDatabase() {
public abstract fun rallyDAO():RallyDAO
}
Inside DAO
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun addVehicleListItem(vehicleListItem:VehicleListItem)
#Query("select * from vehicles_submitted where vehicle_id LIKE :vehicleID")
fun getVehicleListItem(vehicleID:String):VehicleListItem
VehicleListItem Entity
#Entity(tableName = "vehicles_submitted",
foreignKeys = arrayOf(ForeignKey(entity = Rally::class,
parentColumns = arrayOf("rally_id"),
childColumns = arrayOf("rally_id"))))
class VehicleListItem {
#PrimaryKey
#ColumnInfo(name = "vehicle_id")
#SerializedName("vehicle_id")
var vehicleID : String = ""
#ColumnInfo(name = "driver_id")
#SerializedName("driver_id")
var driverID : String = ""
#ColumnInfo(name = "vehicle_name")
#SerializedName("vehicle_name")
var vehicleName : String = ""
#ColumnInfo(name = "driver_name")
#SerializedName("driver_name")
var driverName : String = ""
#ColumnInfo(name = "driver_email")
#SerializedName("driver_email")
var driverEmail : String = ""
#ColumnInfo(name = "rally_id")
#SerializedName("rally_id")
var rallyID: String = ""
#ColumnInfo(name = "is_passed")
#SerializedName("is_passed")
var isPassed = false
#ColumnInfo(name = "passing_time")
#SerializedName("passing_time")
var passingTime:String=""
}
The problem was in the thread.
Room doesn't allow you to run database queries in main thread. The call to insert method was inside of a try-catch block, and I was ignoring the exception.
I fixed it, and here's how it reads right now.
doAsync {
val addedID = appContext.db.rallyDAO().addVehicleListItem(vehicle)
Logger.d("vehicle_lsit_item","Inserted ID $addedID")
}
Also, I reviewed the documentation, the insert method may return a Long (or List of Long in case of List passed to insert). I also changed the signature of insert, and here is the modified code
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun addVehicleListItem(vehicleListItem:VehicleListItem):Long
P.S. I am using anko for doAsync
You can use Stetho to see your DB and Shared pref file in chrome dev tool
http://facebook.github.io/stetho/
EDIT: AS has a built-in DB tool now
An option you may consider, if you have not access permission issues, is to navigate directly into your sqlite instance of the device and query the tables directly there.
If you use the emulator intehgrated with Android Studio or a rooted phone you can do the following:
adb root
adb remount
cd data/data/path/of/your/application/database
sqlite3 mydb.db
Then you can query your tables
There are two cases regarding insertion.
Dao methods annotated with #Insert returns:
In case the input is only one number it returns a Long, check whether this Long equals One, if true then the Item was added successfully.
#Insert
suspend fun insert( student: Student): Long
In case the input is varargs it returns a LongArray, check whether the size of varargs equals the LongArray size, if true then items were added successfully
#Insert
suspend fun insert(vararg students: Student): LongArray
There are multiple ways you can test that.
As #Ege Kuzubasioglu mentioned you can use stetho to check manually (Need minor change to code).
Pull database file from "data/data/yourpackage/databases/yourdatabase.db" to your local machine and use any applications to read the content inside the database. I personally use https://sqlitebrowser.org/.
Pulling database file can be done either using the shell commands or use "Device File Explorer" from android studio.
Write TestCase to see if it is working. Here is an example of test case from one of my projects.
// Code from my DAO class
#Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract Long[] insertPurchaseHistory(List MusicOrders);
//My Test Case
#Test
public void insertPurchaseHistoryTest() {
// Read test data from "api-responses/music-purchase-history-response.json"
InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("api-responses/music-purchase-history-response.json");
// Write utility method to convert your stream to a string.
String testData = FileManager.readFileFromStream(inputStream);
// Convert test data to "musicOrdersResponse"
MusicOrdersResponse musicOrdersResponse = new Gson().fromJson(testData,MusicOrdersResponse.class);
// Insert inmateMusicOrders and get the list of
Long[] rowsInserted = tracksDao.insertPurchaseHistory(musicOrdersResponse.getmusicOrders());
assertThat(rowsInserted.length,Matchers.greaterThan(0));
}
You can use Android Debug Database to access your application's databases via a web browser. Android Debug Database exposes the databases via an embedded web server.
Use it as follows:
Include it as debugImplementation dependency in your app's build.gradle so that it will only be included in debug build and not in release build:
debugImplementation 'com.amitshekhar.android:debug-db:1.0.3'
Start the debug build of your app
The embedded web server launches automatically and announces its address and port in the logs:
D/DebugDB: Open http://XXX.XXX.X.XXX:8080 in your browser
If you are running the app over USB, setup portforwarding:
adb forward tcp:8080 tcp:8080
If you are not running the app over USB, your Android phone and workstation need to be in the same network and your workstation should be able to ping the Android phone.
Finally, open the link from the logs in the browser.
Note that it works just fine without rooting the device as the web server runs inside your app context.
One of the easiest way is:
First download DB Navigator as plugin in Android Studio(For more visit: https://plugins.jetbrains.com/plugin/1800-database-navigator).
Go to view> Tools Window> Device File explorer and download the instance of your DB.
Open DB Browser(From left Pane window) and browse your DB instance you have downloaded. Setup the connection and you are good to browser you database.
For more info please refer to the link provided above.
I make like this(I use coroutines suspend):
class SaveVehicle(val repository: RepositoryInterface) {
suspend fun invoke(vehicleListItem: VehicleListItem): VehicleListItem =
with(vehicleListItem) {
also {
repository.saveVehicle(vehicleListItem)
}
}
}
Kotlin Doc
inline fun <T> T.also(block: (T) -> Unit): T
Calls the specified function block with this value as its argument and returns this value.
also is good for performing some actions that take the context object as an argument. Use also for actions that need a reference rather to the object than to its properties and functions, or when you don't want to shadow this reference from an outer scope.
You can check my example in GitHub:
Related
I am at my wits end with a problem I cannot resolve - when trying to change the schema/data of a table in my database. I am pre-populating the database with Rooms .createFromAsset method.
Current schema and data (working) - as shown in DB Browser - this is the database used to Pre-Populate the apps database.
As I build the database with this code:
val instance = Room.databaseBuilder(
context.applicationContext,
MetarDatabase::class.java,
"metar_database"
)
.createFromAsset("database/metar_database.db")
.fallbackToDestructiveMigration()
.build()
And this data class:
#Entity(tableName = "airport_table")
data class Airport(
#PrimaryKey(autoGenerate = false)
val icao : String,
val name : String?,
val municipality : String?,
val scheduled : Boolean,
val iata : String?
)
It successfully pre-populates the database into the app: as seen on Database Explorer:
The problem is this: If I try and change the schema at all: adding columns, removing columns (done by changing a csv file and importing into a new table in DB Browser, then re-naming to airport_table and adding one to the database version), it does not load the data.
Eg: (Does NOT pre-populate):
#Entity(tableName = "airport_table")
data class Airport(
#PrimaryKey(autoGenerate = false)
val icao : String,
val name : String?
)
I get NO errors apart from "E/libc: Access denied finding property "ro.serialno"" which I don't think is relevant.
In addition - changing this one table means that my other table does not pre-populate.
However the empty database has still been made successfully, and I can add to it within the app.
Please help me - I've looked around stack overflow for similar questions. Some are similar, but involve getting anything to happen at all - my problem is it only works with one exact schema that I happen to succeed with whilst testing - but I want another schema.
Thanks a lot,
Liam
If you intend to export a file from the Room DB or access information using a database browser then use .setJournalMode(JournalMode.TRUNCATE) first. To ensure all data is fully preserved. I guess this is one of the possible causes of this error. Hope can help you
val instance = Room.databaseBuilder(
context.applicationContext,
MetarDatabase::class.java,
"metar_database"
)
.createFromAsset("database/metar_database.db")
.fallbackToDestructiveMigration()
.setJournalMode(JournalMode.TRUNCATE) // Add this line
.build()
After a long time I realised the problem.
I thought I did not need migration as I only wanted to change the pre-populating database, and not migrate anything currently on the device. However the use of destructive migration also deletes your pre-populating data when you add one to the schema number.
So simply add an empty migration and it works
val migration1: Migration = object : Migration(5, 6) { override fun migrate(database: SupportSQLiteDatabase) {}}
Thanks very much to myself for sorting my problem.
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.
I was recently working on an Android app and ran into the error Entities and POJOs must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type) when adding Query's to my Room database repository class.
However, this took me quite a while to spot, because the error I got when building was, to my knowledge, quite unrelated. When searching SO for this question, I found a lot of answers which list something along the likes of 'Make sure you annotate your variables with #ColumnInfo' or 'Move your #Ignore tags to the body of the function' or 'Make an empty constructor like it says' but none of these were applicable to me or fixed the problem.
The weird thing was, my App ran fine if I just removed these three queries from my program, so it couldn't have anything to do with the constructor message.
Eventually, I noticed that my database Query was wrong (see comment in below code). This took me much longer than it should have though, since I had no clue my Query was causing a seemingly unrelated buildlog error.
I'm mostly posting this question here so that someone else might find it if they find themselves in a similar situation (ENSURE YOUR QUERIES ARE CORRECT!!), but I am also wondering if I'm understanding this error message wrong, or if it's actually a faulty error message.
Can someone explain to me why a faulty quert would result into said error message?
DAO containing faulty queries:
#Dao
interface GameItemDAO {
#Query("SELECT * FROM gameItemTable")
suspend fun getAllGameItems(): List<GameItem>
#Insert
suspend fun insertGameItem(gameItem: GameItem)
#Delete
suspend fun deleteGameItem(gameItem: GameItem)
#Query("DELETE FROM gameItemTable")
suspend fun deleteAllGameItems()
#Query("SELECT * FROM gameItemTable ORDER BY ID DESC LIMIT 1")
suspend fun getLastGameItem(): GameItem?
// These functions were faulty, and should have listed SELECT COUNT (*) FROM...
#Query("SELECT * FROM gameItemTable WHERE OUTCOME=0")
suspend fun getWinCount(): Int
#Query("SELECT * FROM gameItemTable WHERE OUTCOME=1")
suspend fun getLossCount(): Int
#Query("SELECT * FROM gameItemTable WHERE OUTCOME=2")
suspend fun getDrawCount(): Int
}
#Entity class:
#Entity(tableName = "gameItemTable")
data class GameItem(
#ColumnInfo(name = "date")
var date: Date,
#ColumnInfo(name = "moveComputer")
var moveComputer: MoveType,
#ColumnInfo(name = "movePlayer")
var movePlayer: MoveType,
#ColumnInfo(name = "outcome")
var outcome: OutcomeType,
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id")
var id: Long? = null
)
The error message is quite correct. Please note that when the return type of a query is not a collection/array/list of some type, Room will return the first item found, though it will retrieve all the data internally so it's not a good practice to depend on that behavior.
In your faulty case, you're asking Room to fetch * which means that it is expecting to return an object of a type with properties matching the 5 columns of the table. Also, it needs to know how to build it, either using a constructor with parameters matching the five columns or an empty constructor with setters for the five columns. Of course, Int doesn't match.
I'm new to android programming and want to try to learn best practices. My first app I'm building is a podcast app to display podcasts from an rss feed and play them. What I have so far is working, but I know I can make it work better.
I'm using a Room Database with a Repository pattern, which might be overkill because I probably don't need to persist the podcast list across app death if I'm just going to re-parse the feed on startup. In my repository class I'm calling my FetchRSS class to do the network call in the init{ } block which returns a List<Podcast>.
I know I'm not doing something right.
In my PodcastDao, I have to use #Insert(onConflict = OnConflictStrategy.REPLACE) because the database already exists and I get an SQL error 1555 regarding duplicate primary key ids. Logically, it'd be better to have a check to see if the entry to be added is already in the database, but I'm not sure how to go about doing that. Or, illogically, clear the database on app death, but then why bother with a database at all. Ideally, I'd like to have a swipe to update function(even if the RSS only updates at most twice a week), but I'm not sure how best to do that.
If anyone has any thoughts about improving this, or a good book for learning android, I'd be all ears.
Thank you so much to everyone who takes the time to look at this!
PodcastDao.kt
#Dao
interface PodcastDao {
#Query("SELECT * FROM podcast") // get everything from the database
fun getPodcasts(): LiveData<List<Podcast>>
#Query("SELECT * FROM podcast WHERE id=(:id)") // get the specific podcast
fun getPodcast(id: String): LiveData<Podcast?>
// #Insert(onConflict = OnConflictStrategy.REPLACE)
// fun addPodcasts(podcasts: LiveData<List<Podcast>>)
// this causes a build error with the generated PodcastDao.java file
// logcat error: Type of the parameter must be a class annotated with #Entity or a collection/array of it.
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun addPodcast(podcast: Podcast)
}
PodcastRepository.kt
class PodcastRepository private constructor(context: Context) {
private lateinit var podcasts: List<Podcast>
init {
CoroutineScope(Dispatchers.Main).launch {
podcasts = FetchRSS().fetchRss() // executes on Dispatchers.IO and returns parsed rss List<Podcast>
// this seems silly to add them one at a time, especially since the list is rather large
for (pod in podcasts) {
addPodcast(pod)
}
//it seems a better choice to dump the full list into the database at once
//however I can't figure out how to put the List<Podcast> into a LiveData<List<Podcast>> object
//or maybe I'm misunderstanding something about LiveData<>
//addPodcasts(podcasts)
}
}
suspend fun addPodcast(podcast: Podcast){
withContext(Dispatchers.IO){
podcastDao.addPodcast(podcast)
}
// this needs to receive the LiveData<List<Podcast>>, or a List<Podcast> and cram it into LiveData<>?
// suspend fun addPodcasts(podcasts: LiveData<List<Podcast>>) {
// withContext(Dispatchers.IO){
// podcastDao.addPodcasts(podcasts)
// }
// }
}
fun addPodcasts(podcasts: LiveData<List<Podcast>>)
should be
fun addPodcasts(podcasts: <List<Podcast>>)
So, now you can call podcastDao.addPodcasts(podcasts) (where podcasts is of type List<Podcast>>) from inside your repository instead of inserting them one by one through a for loop.
You cannot insert a LiveData into Room, only objects marked with #Entity. You can, however, have a query return a LiveData with a List of those entities. You can also return just a List as well.
I am trying to have this new feature working but I keep getting an empty database. There is not much documentation and the articles on medium are not detailed enough.
Using the debugImplementation com.amitshekhar.android:debug-db:1.0.6 plugin to look into my database and manually add an entry to it.
After doing so I download the database.
The file itself is not a *.db file but I change the name and put the .db extension (I have tried both with and without).
I add it to my assets folder and on my databasebuilder I add a .createFromAsset with the path so that I can use the database.
When restarting the app (deleting and reinstalling) I find that is it not pre populated.
What exactly am I doing wrong?
This is how I create the database. There is nothing special with it.
single(createdAtStart = true) {
Room.databaseBuilder(
get(),
CountryDatabase::class.java,
"country_database"
).createFromAsset("database/country_database.db").build()
}
single(createdAtStart = true) {
get<CountryDatabase>().countryDao()
}
#Database(entities = [LocalCountryData::class], version = 4)
abstract class CountryDatabase : RoomDatabase() {
abstract fun countryDao(): CountryDao
}
#Entity(tableName = "countries")
data class LocalCountryData(
#PrimaryKey
val country_name: String,
val country_short_code: String,
val regions_name: String,
val regions_short_code: String
)
When restarting the app I find that is it not pre populated.
createFromAssets will only be invoked and copy the database from the database folder of the assets folder if there is no existing database.
Rather than restarting the App, you should either :-
delete the App's data or
uninstall the App
and then run the App.