clamum
clamum

Reputation: 1364

Android Room - Query Not Calling Expected Constructor

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.

Upvotes: 0

Views: 402

Answers (1)

MikeT
MikeT

Reputation: 56928

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.

Upvotes: 1

Related Questions