Room assigns wrong values when assembling pojos from a query

Viewed 33

I was developing a database for my app. I have the following pojos:

abstract class BaseEntity {
    abstract val id: Long
}

@Entity(
    tableName = EntryDao.TABLE_NAME,
    foreignKeys = [ForeignKey(
        entity = HabitInstance::class,
        parentColumns = arrayOf("id"),
        childColumns = arrayOf("parentHabitInstanceId"),
        onDelete = CASCADE
    )], indices = [Index("completedAt")]
)
@Parcelize
data class Entry(
    @ColumnInfo(index = true) val parentHabitInstanceId: Long,
    val completedAt: Instant,
    @PrimaryKey(autoGenerate = true)
    override val id: Long = 0
) : BaseEntity(), Parcelable

@Entity(
    tableName = HabitInstanceDao.TABLE_NAME,
    foreignKeys = [ForeignKey(
        entity = Ritual::class,
        parentColumns = arrayOf("id"),
        childColumns = arrayOf("parentRitualId"),
        onDelete = CASCADE
    ), ForeignKey(
        entity = Habit::class,
        parentColumns = arrayOf("id"),
        childColumns = arrayOf("parentHabitId"),
        onDelete = CASCADE
    )],
    indices = [Index("parentRitualId"), Index("parentHabitId")]
)
@Parcelize
data class HabitInstance(
    val parentRitualId: Long,
    val parentHabitId: Long,
    @PrimaryKey(autoGenerate = true)
    override val id: Long = 0
) : BaseEntity(), Parcelable

data class Ritual( 
@PrimaryKey(autoGenerate = true)
    override val id: Long = 0
) : Parcelable, BaseEntity()

I needed a query to retrieve Entries based on 2 things: the id of the ritual that they belong to, and a completion time. After much struggle and trying to avoid custom queries, I did not manage to accomplish this and wrote my own query. The method in the dao is as follows:

@Query("""
        SELECT * FROM $TABLE_NAME AS E
        LEFT JOIN ${HabitInstanceDao.TABLE_NAME} AS I ON E.parentHabitInstanceId = I.id
            WHERE (E.completedAt >= :after) AND (I.parentRitualId=(:ritualId) )""")
    abstract fun getAfterByRitual(ritualId: Long, after: Instant): Flow<List<Entry>>

With SQL syntax formatting:

SELECT * FROM Entry AS E
        LEFT JOIN HabitInstance AS I ON E.parentHabitInstanceId = I.id
            WHERE (E.completedAt >= :after) AND (I.parentRitualId=(:ritualId) )

I'm using a generic dao implementation and all of my Entities have the id field derived from BaseEntity. And this is where the problem arises. Running the query I specified above for some reason makes Room confuse the id fields when assigning them. This means, that the query works well and returns values as expected, however all of the Entries returned by it will have not their own id, but rather the id of the habit instance they belong to, basically, parentHabitInstanceId will be equal to id. I have confirmed this behavior by running instrumented tests and debugging the app. I would have changed the name of the id field to be unique, however, my generic dao implementation would not allow it. I thought it's a bug in Room and tried using versions 2.4.0-alpha03 and 2.3.0 to no avail. The room produces no warnings and no errors, however, several entries have the same id, which should be impossible. How can I fix this issue? This is the code of the EntryDao_Impl.java generated by Room:

final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
          try {
            final int _cursorIndexOfParentHabitInstanceId = CursorUtil.getColumnIndexOrThrow(_cursor, "parentHabitInstanceId");
            final int _cursorIndexOfCompletedAt = CursorUtil.getColumnIndexOrThrow(_cursor, "completedAt");
            final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "id");
            final int _cursorIndexOfId_1 = CursorUtil.getColumnIndexOrThrow(_cursor, "id");
            final List<Entry> _result = new ArrayList<Entry>(_cursor.getCount());
            while(_cursor.moveToNext()) {
              final Entry _item;
              final long _tmpParentHabitInstanceId;
              _tmpParentHabitInstanceId = _cursor.getLong(_cursorIndexOfParentHabitInstanceId);
              final Instant _tmpCompletedAt;
              final Long _tmp_1;
              if (_cursor.isNull(_cursorIndexOfCompletedAt)) {
                _tmp_1 = null;
              } else {
                _tmp_1 = _cursor.getLong(_cursorIndexOfCompletedAt);
              }
              _tmpCompletedAt = __dBConverters.toInstant(_tmp_1);
              final long _tmpId;
              _tmpId = _cursor.getLong(_cursorIndexOfId);
              final long _tmpId_1;
              _tmpId_1 = _cursor.getLong(_cursorIndexOfId_1);
              _item = new Entry(_tmpParentHabitInstanceId,_tmpPointsDelta,_tmpJournal,_tmpCompletedAt,_tmpId);
              _result.add(_item);
            }
            __db.setTransactionSuccessful();
            return _result;

Where we clearly see that it simply silently appends the _1 to the id, but confuses these ids eventually. How can I help room differentiate the ids without renaming columns in Entity declarations?

1 Answers

I have found a way to fix the query. The simple fix is to specify the columns needed for inclusion in a POJO explicitly. So, my query became:

SELECT E.parentHabitInstanceId, E.completedAt, E.id FROM $TABLE_NAME AS E
        LEFT JOIN ${HabitInstanceDao.TABLE_NAME} AS I ON E.parentHabitInstanceId = I.id
            WHERE (E.completedAt >= :after) AND ( I.parentRitualId=(:ritualId) )

And this code now passes all tests. Using SELECT * in production code is a bad practice and I've forgotten about it because * is widely used with Room specifically. However, I still think that this is a bug in Room. There is nothing that room can not handle by itself when running my initial query.

Related