Home > Mobile >  Android Studio Type Converters with complex objects
Android Studio Type Converters with complex objects

Time:03-20

While trying to add persistence to my AndroidStudio app using Room, I've encountered with this problem:

error: Cannot figure out how to save this field into database. You can consider adding a type converter for it. private User creator;

I've tried with the type converter said and that's how's looking

class Converters {
    @TypeConverter
    fun usuarioToString(user: User): String {
        return user.getID().toString()
    }

    @TypeConverter
    fun stringToUser(value: String?): UUID {
        return UUID.fromString(value)
    }
}
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract val appDAO: AppDAO
    ...
}
@Entity(tableName = "element_table")
abstract class Element (
    private var IDClass: Int,
    @TypeConverters(Converters::class)
    private var creator: User
    )

I would like to maintain the complex objects within the classes, but I can't understand why typeconverters it's not working properly.

PD: I've made both User and Element classes

CodePudding user response:

You have a number of issues.

Issue 1 - private var .... versus just var ....

Typically, within a Data Class just var .... is used. However, if made private then for room you need to have getters and setters so as to be able to access the member variables.

So you could use :-

@Entity(tableName = "element_table")
abstract class Element (
    @PrimaryKey
    private var IDClass: Int,
    //@TypeConverters(Converters::class) // not needed here as defined at the @Database level
    private var creator: User
) {
    fun getIDClass(): Int {
        return IDClass
    }

    fun setIDClass(id: Int) {
        this.IDClass = id
    }

    fun getCreator(): User {
        return creator
    }

    fun setCreator(user: User) {
        this.creator = user
    }
}

or alternately (in which case there is no need for getters and setters as the values are directly accessible):-

@Entity(tableName = "element_table")
abstract class Element (
    @PrimaryKey
    var IDClass: Int,
    //@TypeConverters(Converters::class) // not needed here as defined at the @Database level
    var creator: User
)

The second issue with the TypeConverter is that you need a matching pair. That is one to convert from the Object and the other to convert to the Object.

The former, the fromObject must take the Object and convert it to a type that room can operate on :-

  • String,
  • integer type such as Long or Int,
  • decimal type (REAL in SQLite terms) such as Double, Float,
  • byte array (BLOB in QLite terms) such as ByteArray

The latter, the toObject, must take the value from the database (therefore the resultant type of the fromObject type converter) and convert it to the Type.

So you have:-

@TypeConverter
fun usuarioToString(user: User): String {
    return user.getID().toString()
}

BUT!! You do not have the matching converter. The matching converter must be passed a String (result of the fromObject converter usuarioToString ) and return a User.

Thus instead of (which returns a UUID object) :-

@TypeConverter
fun stringToUser(value: String?): UUID {
    return UUID.fromString(value)
}

You could have :-

@TypeConverter
fun stringToUser(value: String?): User {
    
    return User(????) /* construct the User based upon the String as an ID */
}

The function/converter :-

@TypeConverter
fun stringToUser(value: String?): UUID {
    return UUID.fromString(value)
}

Would convert from a String to a UUID not a User so with what you have provided it has no use.

However, there is no need to convert an Int to a String an Int can be stored. But as just storing the id sufficient, if the User has a name and or other information would the id converted from an Int to a String be sufficient to build an actual User (hence the User(????)).

As an example say the User class is :-

data class User(
    val id: Int,
    val userName: String

) {

    fun getID(): Int {
        return id
    }
}

Then it would be hard, it not impossible, to generate the userName based upon just the id. There are two basic options to store both the id and the userName (and other details).

You could have a more complex TypeConverter that converts ALL of the member variables (id and userName) into a String or you could have a separate table for the User and have a relationship between the Element and the User.

In relational database terms (SQLite and therefore Room is a relational database) the latter would be considered the preferable. However, often, the case is to forgo the relational aspect of the database and store the relational data within a single column and often JSON Strings are used to store such data.

Example storing the User as a JSON String

first you would need to add a dependency for GSON e.g. :-

implementation 'com.google.code.gson:gson:2.9.0'

then your TypeConverters could be :-

class Converters {
    @TypeConverter
    fun usuarioToString(user: User): String {

        return Gson().toJson(user)
        //return user.getID().toString()
    }

    @TypeConverter
    fun stringToUsuario(value: String): User {
        return Gson().fromJson(value,User::class.java)
        //return User(value.toInt(),"blah")
    }

}

This will then save ALL of the user information not just the id e.g. something like :-

enter image description here

  • As can be seen the same user has been stored twice which contradicts the not duplicating data aspect of normalisation. However, it is convenient (at least at first) but could prove unwieldly especially if there were the need to search data based upon user information.

Example storing User in a table and utilising a relationship between Element and User

Instead of having a String for the JSON representation the Element will store the id of the respective User so the Element class could be:-

@Entity(
    tableName = "element_table",
    )
data class Element (
    @PrimaryKey
    val IDClass: Int,
    @ColumnInfo(index = true) /* optional but negates warning */
    val creator: Int
)

As the User will be a table the User class could be :-

@Entity(tableName = "user_table")
data class User(
    @PrimaryKey
    val id: Int,
    val userName: String
)

As you want to retrieve an Element with the User then an additional POJO (i.e. not an @Entity annotated class) is used e.g. :-

data class ElementWithUser (

    @Embedded
    var element: Element,
    @Relation(
        entity = User::class,
        parentColumn = "creator",
        entityColumn = "id"
    )
    var user: User
)
  • This introduces the @Embedded and the @Relation annotations
    • @Embedded basically copies the embedded class

    • @Relation is used to tell room how to get the related item

      • entity is the class (table) from which to get the related data
      • parentColumn is the column in the parent class (@Embedded) that maps/references the child(ren) in the other table
      • childColumn is the column in the other table that is referenced (most frequently the Primary key)

To use the above you would need to be able to:-

  • insert Users into the user_table
  • insert Elements into the element_table
  • have means of extracting the data

Putting this together a single @Dao annotated interface (or abstract class) :-

@Dao
interface ElementDao {
    @Insert
    fun insert(element: Element): Long
    @Query("SELECT * FROM element_table")
    fun getAllElements(): List<Element>
    @Insert
    fun insert(user: User): Long
    @Query("SELECT * FROM user_table")
    fun getAllUsers(): List<User>
    @Transaction
    @Query("SELECT * FROM element_table")
    fun getAllElementsWithUser(): List<ElementWithUser>
}

For Room you need an @Database annotated class and also the means to actually build the database. In this example :-

@Database(entities = [Element::class,User::class], version = 1, exportSchema = false)
//@TypeConverters( value = [DateTypeConverter::class, Converters::class]) //<<<<< not needed
abstract class TheDatabase: RoomDatabase() {
    abstract fun getElementDao(): ElementDao

    companion object {
        private var instance: TheDatabase? = null
        fun getInstance(context: Context): TheDatabase {
            if (instance == null) {
                instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
                    .allowMainThreadQueries()
                    .build()
            }
            return instance as TheDatabase
        }
    }
}
  • note .alowMainThreadQueries used for brevity and convenience

Last obviously you need code to do anything so for the example:-

class MainActivity : AppCompatActivity() {
    lateinit var db: TheDatabase
    lateinit var dao: ElementDao
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = TheDatabase.getInstance(this)    
        dao = db.getElementDao()
        dao.insert(User(1,"Fred Bloggs"))
        dao.insert(User(2,"Jane Doe"))
        dao.insert(User(3,"Mary Smith"))

        dao.insert(Element(1000,2 /* Jane Doe */))
        dao.insert(Element(2000,1 /* Fred Bloggs */))
        dao.insert(Element(3000,2 /* Jane Doe also in this Element */))

        for (ewu: ElementWithUser in dao.getAllElementsWithUser()) {
            Log.d("DBINFO","Element is ${ewu.element.IDClass} User is ${ewu.user.userName}")
        }
    }
}

Result

The log includes:-

D/DBINFO: Element is 1000 User is Jane Doe
D/DBINFO: Element is 2000 User is Fred Bloggs
D/DBINFO: Element is 3000 User is Jane Doe

The database, via App Inspection can be seen to be:-

enter image description here

and for the user_table :-

enter image description here

In comparison to the version that stored the User as JSON you can see that a User's data is stored just the once (even though Jane Doe is referenced twice).

  • Related