Android room persistent library - how to insert class that has a List object field
AndroidAndroid RoomAndroid 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:
How to parse a list? #78 (answered by Jake Wharton)
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 {
....
}