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 :-
- 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:-
and for the user_table :-
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).