Home > OS >  Saving complex Class data in Room - "Cannot figure out how to save this field into database. Yo
Saving complex Class data in Room - "Cannot figure out how to save this field into database. Yo

Time:01-14

I started to learn Room and I'm facing an issue:

Given two classes, one is a Car, and the other one is an Engine iside a Car.

@Entity
class Car{

    @PrimaryKey
    var id = 0
    var name: String? = null
    var engine: Engine? = null
}

...

@Entity
class Engine{

    @PrimaryKey
    var id = 0
    var manufacturer: String? = null
}

I also have these classes initalized to tables in my AppDatabase class.

@Database(entities = [Car::class, Engine::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
//...
}

The problem is whenever I simply want to run the project I get the following error message which points to the Car's engine field:

Cannot figure out how to save this field into database. You can consider adding a type converter for it.

Is there no simple way for this? I'm looking for something which saves my data with the least amount of code, like Firestore which do all the work with simple annotations.

Thanks in advance.

CodePudding user response:

AS a car would only have a single engine and that you have a table for the engine as well as a table for the car. Then you have a 1 to many relationship. That is a car can have an engine but the same engine can be used by many cars.

So instead of trying to embed the engine within the car you make a relationship, the car (the child) referencing the engine (the parent).

This is as simple as changing the Car to be:-

@Entity
class Car{

    @PrimaryKey
    var id = 0
    var name: String? = null
    var engine: Int? = null
}

An alternative, that would not need the relationship nor a TypeConverter would be to not have the Engine as a table but to use the @Embedded annotation prior to the engine. e.g.

@Entity
class Car{

    @PrimaryKey
    var id = 0
    var name: String? = null
    @Embedded
    var engine: Engine? = null
}

...

class Engine{
    @PrimaryKey
    @ColumnInfo(name = "engineId")
    var id = 0
    var manufacturer: String? = null
}
  • the name of the column used to store the Engine's id changed as otherwise there would be 2 columns with the same name.

  • Note that with this way there is no need for the @Entity annotation as you are storing the Engine values within the Car.

  • This is not considered good practice as if the same engine is used by many cars then you are duplicating data and thus that it is not normalised.

The third and least desirable way from a database perspective is to store a representation of the engine object in a single column. That is to convert the object into a singular storable representation. Typically a JSON string. Thus you need code (a function) to convert from the object to the single value (JSON string) and (another function) to convert from the JSON String to the Object.

With this method not only are you not normalising the data but additionally you end up storing the bloat required to enable the object to be represented. That bloat, from a database, perspective, obfuscating the actual useful stored data to some extent.

In addition there is not a single set/standard library providing the functionality of converting objects to/from JSON, so you have to select a flavour and then include that library in the project.

Here is a class that contains Type Converters that could be used (see comment re library):-

class CarAndEngineTypeConverters{
    /* Using Library as per dependency implementation 'com.google.code.gson:gson:2.10.1' */
    @TypeConverter
    fun convertEngineToJSONString(engine: Engine): String = Gson().toJson(engine)
    @TypeConverter
    fun convertJSONStringToEngine(jsonString: String): Engine = Gson().fromJson(jsonString,Engine::class.java)
}

This would suit your original classes.

Room needs to be told to use these classes (it works out when) via a @TypeConverters annotation (note the plural and not singular) this it immediately before or after the @Database annotation has the highest level of scope. The annotation itself could be @TypeConverters(value = [CarAndEngineTypeConverters::class])

To demonstrate all three together consider this over the top Car class:-

@Entity
class Car{

    @PrimaryKey
    var id = 0
    var name: String? = null
    var engine: Int? = null
    @Embedded
    var alternativeEngine: Engine? = null
    var jsonConvertedEngine: Engine? = null
}
  • Over the top as the engine is stored 3 times (could be different engines)

The *Engine class

@Entity
class Engine{
    @PrimaryKey
    @ColumnInfo(name = "engineId")
    var id = 0
    var manufacturer: String? = null
}

The Type Converters as above.

With the above in place and using within an activity (noting that for brevity/convenience .allowMainThreadQueries has been used):-

    db = TheDatabase.getInstance(this)
    carAndEngineDAO = db.getCarAndEngineDAO()

    var engine1 = Engine()
    engine1.manufacturer = "Ford"
    engine1.id = carAndEngineDAO.insert(engine1).toInt()

    var car1 = Car()
    car1.name = "Escort"
    car1.engine = engine1.id /* id of the engine */
    car1.alternativeEngine = engine1
    car1.jsonConvertedEngine = engine1
    carAndEngineDAO.insert(car1)

Using Android Studios App inspection the view the database then

enter image description here

  • The Columns id and name and obviously as expected
  • The engine column contains the value 0, this is the id of the respective engine in the engine table (maximum 8 bytes to store the id)
  • The JsonConvertedEngine column stores the JSON representation of the Engine (31 bytes)
  • The engineId column and manufacturer column stores the respective values (12 bytes).

The Engine Table (only needed for the relationship) is :-

enter image description here

CodePudding user response:

You should use TypeConverters:

  1. At first add this dependency to your project to convert Engine to Json and vice versa

implementation 'com.squareup.retrofit2:converter-gson:2.5.0'

  1. Now you should create an Object class that convert Engine to Json. This class make Engine understandable for Room :

    object CommonTypeConverters {
    
    @TypeConverter
    @JvmStatic
    fun stringToEngine(value: String): Engine = fromJson(value)
    
    @TypeConverter
    @JvmStatic
    fun engineToString(items: Engine?): String = toJson(items)
    
    
    inline fun <reified T> toJson(value: T): String {
        return if (value == null) "" else Gson().toJson(value)
    }
    
    inline fun <reified T> fromJson(value: String): T {
        return Gson().fromJson(value, object : TypeToken<T>() {}.type)
    }
    

In the end Engine is not a entity and you should add @Typeconverter annotation to your database class :

@Database(entities = [Car::class], version = 1)
@TypeConverters(CommonTypeConverters::class)
abstract class AppDatabase : RoomDatabase() {
//...
}
  • Related