Android room persistent library - how to insert class that has a List object field

AndroidAndroid Room

Android Problem Overview


In [Android room persistent library][1] how to insert entire Model object into table which has in itself another list.

Let me show you what i mean :

@Entity(tableName = TABLE_NAME)
public class CountryModel {

    public static final String TABLE_NAME = "Countries";

    @PrimaryKey
    private int idCountry;
  
    private List<CountryLang> countryLang = null;

    public int getIdCountry() {
        return idCountry;
    }

    public void setIdCountry(int idCountry) {
        this.idCountry = idCountry;
    }

    public String getIsoCode() {
        return isoCode;
    }

    public void setIsoCode(String isoCode) {
        this.isoCode = isoCode;
    }

    /** 
        here i am providing a list of coutry information how to insert 
        this into db along with CountryModel at same time 
    **/
    public List<CountryLang> getCountryLang() {
        return countryLang;
    }

    public void setCountryLang(List<CountryLang> countryLang) {
        this.countryLang = countryLang;
    }
}

my DAO looks like this:

@Dao
public interface CountriesDao{

    @Query("SELECT * FROM " + CountryModel.TABLE_NAME +" WHERE isoCode =:iso_code LIMIT 1")
    LiveData<List<CountryModel>> getCountry(String iso_code);

    @Query("SELECT * FROM " + CountryModel.TABLE_NAME )
    LiveData<List<CountryModel>> getAllCountriesInfo();

    @Insert(onConflict = REPLACE)
    Long[] addCountries(List<CountryModel> countryModel);

    @Delete
    void deleteCountry(CountryModel... countryModel);

    @Update(onConflict = REPLACE)
    void updateEvent(CountryModel... countryModel);
}

When i call database.CountriesDao().addCountries(countryModel); i get the following room db compile error: Error:(58, 31) error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.

should there be another table called CountryLang ? and if so how to tell room to connect them on insert statement ?

The CountryLang object itself looks like this:

public class CountryLang {

    
    private int idCountry;
    
    private int idLang;
    
    private String name;

    public int getIdCountry() {
        return idCountry;
    }

    public void setIdCountry(int idCountry) {
        this.idCountry = idCountry;
    }

    public int getIdLang() {
        return idLang;
    }

    public void setIdLang(int idLang) {
        this.idLang = idLang;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

the response looks like this:

"country_lang": [
      {
        "id_country": 2,
        "id_lang": 1,
        "name": "Austria"
      }
    ]

For every country so its not going to be more then one item here. Im comfortable desgning it for just one item in the country_lang list. So i can just make a table for country_lang and then some how link it to CountryModel. but how ? can i use foreign key ? i was hoping i did not have to use a flat file. so your saying i have to store it as json ? Is it recommended not to use room for temporary ? what to use instead ? [1]: https://developer.android.com/topic/libraries/architecture/room.html

Android Solutions


Solution 1 - Android

You can easly insert the class with list object field using TypeConverter and GSON,

public class DataConverter {

    @TypeConverter
    public String fromCountryLangList(List<CountryLang> countryLang) {
        if (countryLang == null) {
            return (null);
        }
        Gson gson = new Gson();
        Type type = new TypeToken<List<CountryLang>>() {}.getType();
        String json = gson.toJson(countryLang, type);
        return json;
    }

    @TypeConverter
    public List<CountryLang> toCountryLangList(String countryLangString) {
        if (countryLangString == null) {
            return (null);
        }
        Gson gson = new Gson();
        Type type = new TypeToken<List<CountryLang>>() {}.getType();
        List<CountryLang> countryLangList = gson.fromJson(countryLangString, type);
        return countryLangList;
    }
 }

Next, Add the @TypeConverters annotation to the AppDatabase class

    @Database(entities = {CountryModel.class}, version = 1)
    @TypeConverters({DataConverter.class})
    public abstract class AppDatabase extends RoomDatabase {
      public abstract CountriesDao countriesDao();
    }

For more information about TypeConverters in Room check our blog here and the official docs.

Solution 2 - Android

Here is the Aman Gupta's converter in Kotlin for lazy Googler's who enjoy copy pasting:

class DataConverter {

    @TypeConverter
    fun fromCountryLangList(value: List<CountryLang>): String {
        val gson = Gson()
        val type = object : TypeToken<List<CountryLang>>() {}.type
        return gson.toJson(value, type)
    }

    @TypeConverter
    fun toCountryLangList(value: String): List<CountryLang> {
        val gson = Gson()
        val type = object : TypeToken<List<CountryLang>>() {}.type
        return gson.fromJson(value, type)
    }
}

Also, add the @TypeConverters annotation to the AppDatabase class

@Database(entities = arrayOf(CountryModel::class), version = 1)
@TypeConverters(DataConverter::class)
abstract class AppDatabase : RoomDatabase(){
    abstract fun countriesDao(): CountriesDao
}

Solution 3 - Android

As Omkar said, you cannot. Here, I describe why you should always use @Ignore annotation according to the documentation: https://developer.android.com/training/data-storage/room/referencing-data.html#understand-no-object-references

You will treat the Country object in a table to retrieve the data of its competence only; The Languages objects will go to another table but you can keep the same Dao:

  • Countries and Languages objects are independent, just define the primaryKey with more fields in Language entity (countryId, languageId). You can save them in series in the Repository class when the active thread is the Worker thread: two requests of inserts to the Dao.
  • To load the Countries object you have the countryId.
  • To load the related Languages objects you already have the countryId, but you will need to wait that country is loaded first, before to load the languages, so that you can set them in the parent object and return the parent object only.
  • You can probably do this in series in the Repository class when you load the country, so you will load synchronously country and then languages, as you would do at the Server side! (without ORM libraries).

Solution 4 - Android

You cannot.

The only way to achieve this is to use @ForeignKey constraint. If you want to still keep the list of object inside your parent POJO, you have to use @Ignore or provide a @TypeConverter

For more info, follow this blog post:-

https://www.bignerdranch.com/blog/room-data-storage-on-android-for-everyone/

and sample code:-

https://github.com/googlesamples/android-architecture-components

Solution 5 - Android

I had a similar situation. To solve this, I used TypeConverts and Moshi to parse the list to string.

Follow the steps below:

1 - Create a class with converters.

class Converters {

    private val moshi = Moshi.Builder().build()
    private val listMyData : ParameterizedType = Types.newParameterizedType(List::class.java, MyModel::class.java)
    private val jsonAdapter: JsonAdapter<List<MyModel>> = moshi.adapter(listMyData)

    @TypeConverter
    fun listMyModelToJsonStr(listMyModel: List<MyModel>?): String? {
        return jsonAdapter.toJson(listMyModel)
    }

    @TypeConverter
    fun jsonStrToListMyModel(jsonStr: String?): List<MyModel>? {
        return jsonStr?.let { jsonAdapter.fromJson(jsonStr) }
    }
}

2 - Define the class with Converters in your RoomDatabase class.

@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {...}

> ...you add the @TypeConverters annotation to the AppDatabase class so that Room can use the converter that you've defined for each entity and DAO in that AppDatabase...

> ...sometimes, your app needs to use a custom data type whose value you would like to store in a single database column. To add this kind of support for custom types, you provide a TypeConverter, which converts a custom class to and from a known type that Room can persist.

References:

Moshi Parsing List (Kotlin)

How to parse a list? #78 (answered by Jake Wharton)

Use type converters (official documentation)

Moshi library

Solution 6 - Android

I did something similar to @Daniel Wilson, however, I used Moshi since it is the suggested library. To learn more about the difference between Moshi and Gson I suggest you watch this video.

In my case, I had to store a List<LatLng> inside the Room database. In case you didn't know LatLng is used to handle the geographic coordinates, which means latitude and longitude. To achieve that I used this code:

class Converters {

    private val adapter by lazy {
        val moshi = Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()
        val listMyData = Types.newParameterizedType(List::class.java, LatLng::class.java)
        return@lazy moshi.adapter<List<LatLng>>(listMyData)
    }

    @TypeConverter
    fun toJson(coordinates: List<LatLng>) : String {
        val json = adapter.toJson(coordinates)
        return json
    }

    @TypeConverter
    fun formJson(json: String) : List<LatLng>? {
        return adapter.fromJson(json)
    }
}

Solution 7 - Android

Add @Embedded for the custom object field (refer following eg)

//this class refers to pojo which need to be stored
@Entity(tableName = "event_listing")
public class EventListingEntity implements Parcelable {

	@Embedded  // <<<< This is very Important in case of custom obj 
	@TypeConverters(Converters.class)
	@SerializedName("mapped")
	public ArrayList<MappedItem> mapped;

	//provide getter and setters
	//there should not the duplicate field names
}

//add converter so that we can store the custom object in ROOM database
public class Converters {
    //room will automatically convert custom obj into string and store in DB
	@TypeConverter
	public static String 
	convertMapArr(ArrayList<EventListingEntity.MappedItem> list) {
	Gson gson = new Gson();
	String json = gson.toJson(list);
	return json;
	}

  //At the time of fetching records room will automatically convert string to 
  // respective obj
  @TypeConverter
  public static ArrayList<EventsListingResponse.MappedItem> 
  toMappedItem(String value) {
    Type listType = new 
     TypeToken<ArrayList<EventsListingResponse.MappedItem>>() {
    }.getType();
    return new Gson().fromJson(value, listType);
  }

 }

//Final db class
@Database(entities = {EventsListingResponse.class}, version = 2)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
	....
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionj2emanueView Question on Stackoverflow
Solution 1 - AndroidAman Gupta - ΔMΔNView Answer on Stackoverflow
Solution 2 - AndroidDaniel WilsonView Answer on Stackoverflow
Solution 3 - AndroidDavideasView Answer on Stackoverflow
Solution 4 - AndroidOmkar AmberkarView Answer on Stackoverflow
Solution 5 - AndroidLeonardo CostaView Answer on Stackoverflow
Solution 6 - AndroidMattia FeriguttiView Answer on Stackoverflow
Solution 7 - Androidshashank JView Answer on Stackoverflow