0

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:

Database tables

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.

clamum
  • 1,237
  • 10
  • 18

1 Answers1

1

I believe that your issue is based upon shotCount being @Ignored (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.

  • You could obviously add constructors/functions, as/if required to the Firearm class to handle FirearmTable's

e.g.

@Entity(tableName = "firearm")
data class FireArmTable(
    @ColumnInfo(name = BaseColumns._ID)
    @PrimaryKey
    var id: Long?=null,
    var name: String,
    var notes: String? = null
)
  • using BaseColumns._ID would change the ID column name should it ever change.
  • using Long=null? without 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)
  • the above are just suggestions, they are not required

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.

MikeT
  • 51,415
  • 16
  • 49
  • 68
  • Thank you very much for taking the time to help me with this. I haven't gotten a chance to dig into this and try it out but I will this weekend! – clamum Jun 17 '22 at 14:53
  • Maybe I didn't do something right as I'm getting an error about "The columns returned by the query does not have the fields [firearmImageUrl]" and it references the `Firearm` POJO, which is now the new POJO class I made. There's a `FirearmTable` class as well that has the `@Entity` annotation on it. Probably doing something dumb but this Room stuff confuses the heck outta me. I put the class code in this link, everything else is pretty much the same: https://pastebin.com/Lcn3j9ke. I did some reading and maybe a Multimap return type is better (from "Define relationships between objects" doc)? – clamum Jun 23 '22 at 07:14
  • @clamum There us no FirearmImageUrl in the FirearmTable, so therefore no such column. Options are to include `var firearmImageUrl: String` in FirearmTable class OR to have `@Ignore var firearmUmageUrl: String = ""` *(empty string could be any indicator of no value being supplied)* in the Firearm class. – MikeT Jun 23 '22 at 08:11
  • Tried adding `@Ignore` to `firearmImageUrl` in `Firearm` class and no errors but it still returns empty string for that field. The query will return a value for that `shotCount` field, which is the result of doing a join to a `shot_track` table, but it will not return a value for the `firearmImageUrl` field, which is also just a join to `firearm_image` table. I guess I just am not experienced enough in it, but I think this Room stuff is insane for such a simple thing. Are there any articles/vids I can reference? – clamum Jul 08 '22 at 07:26
  • Adding `firearmImageUrl` to `FirearmTable` don't make sense since I thought it was only supposed to have the actual db table fields. I don't know though. I'm kinda stumped at this point with this whole thing. – clamum Jul 08 '22 at 07:28