Search code examples
androidandroid-room

TypeConverter / mapper for the entire entity class in Room persistence?


I am trying to have a TypeConverter for the entire Entity class. All the examples I have seen so far only apply TypeConverter to individual fields of the entity, so I wonder if this is even possible?

In this contrieved example, I want to work with User class in the Dao, but room should save it as UserEntity

object TestTypeConverter{
    @TypeConverters
    fun entityToUser(entity: UserEntity) = User(entity.id, entity.username, entity.picUrl, SomeBitmapLoader(entity.pic), OtherInfoFromUsername(entity.username))

    @TypeConverters
    fun userToEntity(user: User) = UserEntity(user.id, user.username, user.picUrl)
    
}
    
@Entity(tableName = "users")
data class UserEntity(
        val id: Int,
        val username: String,
        val picUrl: String,
)

@Dao
@TypeConverters(TestTypeConverter::class)
interface UsersDao {
    @get:Query("SELECT * FROM users")
    val all: List<User>
}

I get "Cannot figure out how to read this field from a cursor" when I build.


Solution

  • so I wonder if this is even possible?

    No (with TypeConverters assuming the object is to only store partial data e.g. to save image paths but not the image) but Yes you can get a List<User> (with the missing/un-stored data).

    NO with TypeConverters

    Type Converters are all about individual fields and converting from a complex type to a single type that can be stored as a single column in the database.

    • by complex type, a type that is not :-
      • an integer (INTEGER type in SQLite) type (Int, Long, Byte etc), or
      • a decimal (REAL type in SQLite) type (Double, Float etc), or
      • a String (TEXT type in SQLite), or
      • a ByteArray (BLOB type in SQLite).

    In your case the Entity (table in database terms) consists of Int, and String types, which are types that can be stored in columns without requiring conversion. So the Type converters are redundant.

    Now if you had an entity such as (not that this is probably what you want) :-

    @Entity(tableName = "users")
    data class UserEntity(
            @PrimaryKey
            val id: Int=0,
            val username: String,
            val picUrl: String,
            val user: User //<<<<< TyepConverter required
    )
    

    Then because the user field is a complex type then a TypeConverter would be expected for the user field as it's a complex type.

    However, the TypeConverter userToEntity would NOT be valid/suitable/usable because it converts from a user (complex type) to a userEntity (complex type).

    YES if List<User> is the goal.

    Extracting a List may be possible BUT it is not TypeConverters that would convert the picUrl to the pic (ByteArray/Bitmap). TypeConverters are purely for converting data that is stored between simple and complex types.

    Assuming that the User class is :-

    data class User(
        var id: Int,
        var username: String,
        var picUrl: String,
        var pic: ByteArray,
        var otherInfoFromUserName: String
    )
    

    Then you could get a List if you let Room know that you want to build this from the data extracted.

    What Room needs to have is matching fields and columns. So SELECT * From users will return 3 columns:-

    • id,
    • username, and
    • picUrl

    whilst List<User> will, if as above,want 5 columns.

    You could use a query such as SELECT *, zeroblob(0) AS pic, 'whatever'||username AS otherInfoFromUserName FROM users and this could then return a List BUT pic will be a ByteArray of 0 elements AND otherInforForUserName will be whatever followed by the username.

    For just the 3 columns, you could have @Ignore annotation for the pic and otherInfoFromUserName e.g.

    data class User(
        var id: Int,
        var username: String,
        var picUrl: String,
        @Ignore
        var pic: ByteArray,
        @Ignore
        var otherInfoFromUserName: String
    )
    

    along with a Constructor that allowed construction of a User from those three values. e.g.

    data class User(
        var id: Int,
        var username: String,
        var picUrl: String,
        @Ignore
        var pic: ByteArray,
        @Ignore
        var otherInfoFromUserName: String
    )  {
    
        constructor(id: Int, username: String, picUrl: String): this(id = id, username = username, picUrl = picUrl, pic = byteArrayOf(), otherInfoFromUserName = "")
    }
    

    and return a List<User> in which case pic would be an empty ByteArray and otherInfoFromUserName would be an empty String.

    You could then have functions that resolve the values (pic and otherInfoFromUserName) and then apply them via the constructor e.g.

    data class User(
        var id: Int,
        var username: String,
        var picUrl: String,
        @Ignore
        var pic: ByteArray,
        @Ignore
        var otherInfoFromUserName: String
    )  {
    
        constructor(id: Int, username: String, picUrl: String): this(id = id, username = username, picUrl = picUrl, pic = byteArrayOf(), otherInfoFromUserName = "") {
            this.resolvePic()
            this.resolveOtherInfoFromUser()
        }
    
        fun resolvePic() {
            this.pic = byteArrayOf(0,1,2,3,4,5,6,7,8,9)
        }
        fun resolveOtherInfoFromUser() {
            this.otherInfoFromUserName = "whatever$username"
        }
    }
    

    In which case the values would be resolved via the Constructor.

    Having the resolve functions would also allow a Dao function with a body (in an abstract class as opposed to an interface) such as :-

    @Dao
    abstract class AllDao {
    
        @Insert(onConflict = IGNORE)
        abstract fun insert(userEntity: UserEntity): Long
        @Query("SELECT * FROM users")
        abstract fun getUsersAsListUser(): List<User>
        @Query("SELECT *, zeroblob(0) AS pic, 'whatever'||username AS otherInfoFromUserName FROM users ")
        abstract fun getUserEntityExpandedToUser(): List<User>
    
        @Query("")
        fun getResolvedUserList(): List<User> {
            val userList = getUsersAsListUser()
            for(u: User in userList) {
                u.resolvePic()
                u.resolveOtherInfoFromUser()
            }
            return userList
        }
    }
    

    Example

    Using the last User class and AllDao above then the following :-

        val db = TheDatabase.getInstance(this);
        val dao = db.getAllDao()
    
        dao.insert(UserEntity(1,"User1","image01"))
        dao.insert(UserEntity(2,"User2","image02"))
        dao.insert(UserEntity(3,"User3","image03"))
    
        for (u: User in dao.getUserEntityExpandedToUser()) {
            Log.d("DBINFO_01","Username = ${u.username} PIC URL =${u.picUrl} PIC = ${u.pic} (size = ${u.pic.size}) OTHER = ${u.otherInfoFromUserName} ")
        }
        for (u: User in dao.getUsersAsListUser()) {
            Log.d("DBINFO_01","Username = ${u.username} PIC URL =${u.picUrl} PIC = ${u.pic} (size = ${u.pic.size}) OTHER = ${u.otherInfoFromUserName} ")
        }
        for (u: User in dao.getResolvedUserList()) {
            Log.d("DBINFO_01","Username = ${u.username} PIC URL =${u.picUrl} PIC = ${u.pic} (size = ${u.pic.size}) OTHER = ${u.otherInfoFromUserName} ")
        }
    

    Will result in the Log, after running, including :-

    2022-02-10 07:48:29.525 D/DBINFO_01: Username = User1 PIC URL =image01 PIC = [B@f1e7a3d (size = 10) OTHER = whateverUser1 
    2022-02-10 07:48:29.525 D/DBINFO_01: Username = User2 PIC URL =image02 PIC = [B@166d232 (size = 10) OTHER = whateverUser2 
    2022-02-10 07:48:29.525 D/DBINFO_01: Username = User3 PIC URL =image03 PIC = [B@17eeb83 (size = 10) OTHER = whateverUser3 
    
    
    2022-02-10 07:48:29.526 D/DBINFO_01: Username = User1 PIC URL =image01 PIC = [B@91e6100 (size = 10) OTHER = whateverUser1 
    2022-02-10 07:48:29.527 D/DBINFO_01: Username = User2 PIC URL =image02 PIC = [B@97d7d39 (size = 10) OTHER = whateverUser2 
    2022-02-10 07:48:29.527 D/DBINFO_01: Username = User3 PIC URL =image03 PIC = [B@9abdf7e (size = 10) OTHER = whateverUser3 
    
    
    2022-02-10 07:48:29.528 D/DBINFO_01: Username = User1 PIC URL =image01 PIC = [B@5babcdf (size = 10) OTHER = whateverUser1 
    2022-02-10 07:48:29.528 D/DBINFO_01: Username = User2 PIC URL =image02 PIC = [B@9888d2c (size = 10) OTHER = whateverUser2 
    2022-02-10 07:48:29.528 D/DBINFO_01: Username = User3 PIC URL =image03 PIC = [B@62fb3f5 (size = 10) OTHER = whateverUser3
    

    Other Permutations

    Considering the above then you could question why two classes as clearly a single class could embody both aspects. So you could, for example, have/use :-

    @Entity(tableName = "user_combined")
    data class UserEntityCombined(
        @PrimaryKey
        val id: Long? = null,
        val username: String,
        val picUrl: String,
        @Ignore
        var pic: ByteArray = byteArrayOf(),
        @Ignore
        var otherInfoFromUserName: String = ""
    ) {
        constructor(id: Long?, username: String, picUrl: String): this(id = id, username = username, picUrl = picUrl, pic = byteArrayOf(), otherInfoFromUserName = "") {
            this.resolvePic()
            this.resolveOtherInfoForUserName()
        }
        fun resolvePic() {
            this.pic = byteArrayOf(0,1,2,3,4,5,6,7,8,9)
        }
        fun resolveOtherInfoForUserName() {
            this.otherInfoFromUserName = "whatever${username}"
        }
    }
    

    If you really want two separate classes then you could EMBED the User class (which has the respective constructer and resolve methods), for example :-

    @Entity(primaryKeys = ["id"], tableName = "user_embedded")
    data class UserEntityWithEmbeddedUser(
        @Embedded
        val user: User
    )
    
    • The table names changed so that the above can all be compiled and therefore tested.

    Example

    With both of the above permutations added along with :-

    @Insert(onConflict = IGNORE)
    abstract fun insert(userEntityCombined: UserEntityCombined): Long
    @Query("SELECT * FROM user_combined")
    abstract fun getUserCombined(): List<UserEntityCombined>
    
    @Insert(onConflict = IGNORE)
    abstract fun insert(userEntityWithEmbeddedUser: UserEntityWithEmbeddedUser): Long
    @Query("SELECT * FROM user_embedded")
    abstract fun getUserEmbedded(): List<User>
    

    and :-

        dao.insert(UserEntityCombined(1,"User1","image01"))
        dao.insert(UserEntityCombined(2,"User2","image02"))
        dao.insert(UserEntityCombined(3,"User3","image03"))
    
        dao.insert(UserEntityWithEmbeddedUser(User(1,"User1","image01")))
        dao.insert(UserEntityWithEmbeddedUser(User(2,"User2","image02")))
        dao.insert(UserEntityWithEmbeddedUser(User(3,"User3","image03")))
    

    and :-

        /* UserEntityCombined */
        for (u: UserEntityCombined in dao.getUserCombined() ) {
            Log.d("DBINFO_02","Username = ${u.username} PIC URL =${u.picUrl} PIC = ${u.pic} (size = ${u.pic.size}) OTHER = ${u.otherInfoFromUserName} ")
        }
    
        /* UserEntityWithEmbeddedUser */
        for (u: User in dao.getUserEmbedded()) {
            Log.d("DBINFO_03","Username = ${u.username} PIC URL =${u.picUrl} PIC = ${u.pic} (size = ${u.pic.size}) OTHER = ${u.otherInfoFromUserName} ")
        }
    

    The in addition to the previous log, the log includes :-

    2022-02-10 10:15:20.482 D/DBINFO_02: Username = User1 PIC URL =image01 PIC = [B@7949a71 (size = 10) OTHER = whateverUser1 
    2022-02-10 10:15:20.483 D/DBINFO_02: Username = User2 PIC URL =image02 PIC = [B@5ba6056 (size = 10) OTHER = whateverUser2 
    2022-02-10 10:15:20.483 D/DBINFO_02: Username = User3 PIC URL =image03 PIC = [B@23968d7 (size = 10) OTHER = whateverUser3 
    2022-02-10 10:15:20.486 D/DBINFO_03: Username = User1 PIC URL =image01 PIC = [B@dad55c4 (size = 10) OTHER = whateverUser1 
    2022-02-10 10:15:20.486 D/DBINFO_03: Username = User2 PIC URL =image02 PIC = [B@c876cad (size = 10) OTHER = whateverUser2 
    2022-02-10 10:15:20.486 D/DBINFO_03: Username = User3 PIC URL =image03 PIC = [B@3942be2 (size = 10) OTHER = whateverUser3
    

    Using App Inspection then the tables are basically identical:-

    enter image description here

    enter image description here

    enter image description here

    i.e. all of the above are just different ways to achieve the same end result of storing partial data of an object and with appropriate means of completing the other values of extracting the data as the overarching object.