Reputation: 1364
I am rewriting my old Sqlite Android app that was in Java to be a Jetpack Compose app in Kotlin that uses a Room database.
I've got about half of the app done but now I am seeing a strange behavior where my DAO query is not returning the data it should be, and the cause seems to be because the correct constructor, defined in my data model class, is not being called.
I am pretty sure this constructor WAS being called back before, before I added a new table to the database. I'm not 100% on this but I think so.
Anyway, here's some relevant code:
Database:
Data Model (I've added an @Ignore
property, firearmImageUrl
, for this imageFile
column from the firearm_image
table so it's part of the Firearm
object. Maybe not the best way to do this, for joining tables? But this is a small simple app that like 5 people worldwide might use, more likely just me):
@Entity(tableName = "firearm")
class Firearm {
@ColumnInfo(name = "_id")
@PrimaryKey(autoGenerate = true)
var id = 0
var name: String = ""
var notes: String? = null
@Ignore
var shotCount = 0
@Ignore
var firearmImageUrl: String = ""
@Ignore
constructor() {
}
@Ignore
constructor(
name: String,
notes: String?
) {
this.name = name
this.notes = notes
}
@Ignore
constructor(
name: String,
notes: String?,
shotCount: Int
) {
this.name = name
this.notes = notes
this.shotCount = shotCount
}
@Ignore
constructor(
id: Int,
name: String,
notes: String?,
shotCount: Int
) {
this.id = id
this.name = name
this.notes = notes
this.shotCount = shotCount
}
// THIS IS THE CONSTRUCTOR THAT I **WANT** TO BE CALLED AND IS NOT. THIS USED TO HAVE AN
// @IGNORE TAG ON IT BUT REMOVING IT DID NOTHING
constructor(
id: Int,
name: String,
notes: String?,
shotCount: Int,
firearmImageUrl: String
) {
this.id = id
this.name = name
this.notes = notes
this.shotCount = shotCount
this.firearmImageUrl = firearmImageUrl
}
// THIS IS THE CONSTRUCTOR THAT IS BEING CALLED BY THE BELOW DAO METHOD, EVEN THOUGH
// ITS PARAMETERS DO NOT MATCH WHAT'S BEING RETURNED BY THAT QUERY
constructor(
id: Int,
name: String,
notes: String?,
) {
this.id = id
this.name = name
this.notes = notes
}
}
DAO (I removed the suspend
keyword just so this thing would hit a debug breakpoint; also this query absolutely works, I copy-pasted it into the Database Inspector and ran it against the db and it returns the proper data with firearmImageUrl
populated with a path):
@Query(
"SELECT f._id, " +
"f.name, " +
"f.notes, " +
"CASE WHEN SUM(s.roundsFired) IS NULL THEN 0 " +
"ELSE SUM(s.roundsFired) " +
"END shotCount, " +
"fi.imageFile firearmImageUrl " +
"FROM firearm f " +
"LEFT JOIN shot_track s ON f._id = s.firearmId " +
"LEFT JOIN firearm_image fi ON f._id = fi.firearmId " +
"WHERE f._id = :firearmId " +
"GROUP BY f._id " +
"ORDER BY f.name"
)
fun getFirearm(firearmId: Int): Firearm?
Repo:
override fun getFirearm(firearmId: Int): Firearm? {
return dao.getFirearm(firearmId)
}
Use Case (I'm dumb and decided to do this Clean Architecture but it's way overkill; this is just an intermediate class and calls the Repo method):
data class FirearmUseCases(
/**
* Gets the valid Firearms in the application.
*/
val getFirearms: GetFirearms,
/**
* Gets the specified Firearm.
*/
val getFirearm: GetFirearm
)
class GetFirearm(private val repository: FirearmRepository) {
operator fun invoke(firearmId: Int): Firearm? {
return repository.getFirearm(firearmId)
}
}
ViewModel:
init {
savedStateHandle.get<Int>("firearmId")?.let { firearmId ->
if (firearmId > 0) {
viewModelScope.launch {
firearmUseCases.getFirearm(firearmId)?.also { firearm ->
_currentFirearmId.value = firearm.id
// and so on... point is, the object is retrieved in this block
}
}
}
}
}
What's happening is the DAO is calling the constructor that I've commented above, and not the constructor that has the parameters that match what the query is returning. Not sure why. That constructor did have an @Ignore
tag on it before tonight but I just tried removing it and there was no difference; constructor with only 3 parameters is still being called.
Thanks for any help, this Room stuff is nuts. I should've just stuck with Sqlite lmao. It's such a simple app, the old version was super fast and worked fine. Silly me wanting to learn contemporary design though.
Upvotes: 0
Views: 402
Reputation: 56928
I believe that your issue is based upon shotCount being @Ignore
d (which you obviously want). Thus, even though you have it in the output, Room ignores the column and thus doesn't use the constructor you wish.
I would suggest that the resolution is quite simple albeit perhaps a little weird and that is to have Firearm not annotated with @Entity
and just a POJO (with no Room annotation) and then have a separate @Entity
annotated class specifically for the table.
e.g.
@Entity(tableName = "firearm")
data class FireArmTable(
@ColumnInfo(name = BaseColumns._ID)
@PrimaryKey
var id: Long?=null,
var name: String,
var notes: String? = null
)
autogenerate = true
will generate an id (if no value is supplied) but is more efficient see https://sqlite.org/autoinc.html (especially the very first sentence)and :-
class Firearm() : Parcelable {
@ColumnInfo(name = "_id")
@PrimaryKey(autoGenerate = true)
var id = 0
var name: String = ""
var notes: String? = null
//@Ignore
var shotCount = 0
//@Ignore
var firearmImageUrl: String = ""
....
Using the above and using (tested with .allowMainThreadQueries
) then the following:-
db = TheDatabase.getInstance(this)
dao = db.getFirearmDao()
val f1id = dao.insert(FireArmTable( name = "F1", notes = "Awesome"))
val f2id = dao.insert(FireArmTable(name = "F2", notes = "OK"))
dao.insert(Firearm_Image(firearmId = f1id, imageFile = "F1IMAGE"))
dao.insert(Shot_track(firearmId = f1id, roundsFired = 10))
dao.insert(Shot_track(firearmId = f1id, roundsFired = 20))
dao.insert(Shot_track(firearmId = f1id, roundsFired = 30))
dao.insert(Firearm_Image(firearmId = f2id, imageFile = "F2IMAGE"))
dao.insert(Shot_track(firearmId = f2id, roundsFired = 5))
dao.insert(Shot_track(firearmId = f2id, roundsFired = 15))
logFirearm(dao.getFirearm(f1id.toInt()))
val f1 = dao.getFirearm(f1id.toInt())
val f2 = dao.getFirearm(f2id.toInt())
logFirearm(f2)
}
fun logFirearm(firearm: Firearm?) {
Log.d("FIREARMINFO","Firearm: ${firearm!!.name} Notes are: ${firearm.notes} ImageURL: ${firearm.firearmImageUrl} ShotCount: ${firearm.shotCount}")
}
Where getFirearm is your Query copied and pasted, shows the following in the log:-
D/FIREARMINFO: Firearm: F1 Notes are: Awesome ImageURL: F1IMAGE ShotCount: 60
D/FIREARMINFO: Firearm: F2 Notes are: OK ImageURL: F2IMAGE ShotCount: 20
i.e. Shotcounts as expected.
Upvotes: 1