78

In Android room persistent library 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 ?

Darwind
  • 7,284
  • 3
  • 49
  • 48
j2emanue
  • 60,549
  • 65
  • 286
  • 456
  • forgot about room for temporaray, so in which format you want to store countryLang fields in CountryModel? are you want to store as comma seprated? – Moinkhan Jun 16 '17 at 04:27
  • If you are sure there is only one item then you should use object instead array. because if you use object you can embed the countryLang class easily in CountryModel using `@Embedded` anotation – Moinkhan Jun 16 '17 at 04:48
  • If you can't change json then you should use foreign key relationship. – Moinkhan Jun 16 '17 at 04:54
  • Thanks for the reply. Good idea about @embedded. Do you know if i can use embedded with list ? this model is coming from a rest service response . its returned as json and then i use gson to convert it to java object. So i dont i have to keep it as type List. The json has it as a list object as you see in my statement about the strucutre of country_lang. How to use foreign key in this case. – j2emanue Jun 16 '17 at 04:55
  • There is another way, where you can change your type at time of insertion by using `@TypeConverter` annoatiaon. I am trying it but stuck becuase of Room official bug. – Moinkhan Jun 16 '17 at 04:57
  • maybe i can do it with gson then. good idea about typeConverter. maybe gson has typeConverter then i can change it to an object and then use realm. – j2emanue Jun 16 '17 at 05:12
  • yeah I was thinking about at gson level. Just create a gson converter which will give you class insted array then you can easily embed.And if you will give object insted array no need to use `@TypeConverter` in room, you can just use `@Embeded` annotation. – Moinkhan Jun 16 '17 at 05:15
  • Tell me which bug makes you not able to use room TypeConverter – j2emanue Jun 16 '17 at 05:18
  • I creating converter which convert List to Object but getting this error. `Error:(79, 41) error: modCount has protected access in AbstractList` This error is coming in DAO generted code. – Moinkhan Jun 16 '17 at 05:37
  • 1
    You can use `@Relation` annotation. Check out [this answer](https://stackoverflow.com/questions/44330452/android-persistence-room-cannot-figure-out-how-to-read-this-field-from-a-curso/44424148#44424148) – Devrim Jun 16 '17 at 06:40
  • Duplicate. https://stackoverflow.com/questions/44399380/room-persistence-library-nested-object-with-listvideo-embedded-doesnt-wor/44423285#44423285 – Amit Patel Sep 07 '17 at 03:16
  • Use inside list as a Converter. https://stackoverflow.com/questions/44399380/room-persistence-library-nested-object-with-listvideo-embedded-doesnt-wor/44423285#44423285 – Amit Patel Sep 07 '17 at 03:20

8 Answers8

114

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.

Smirky
  • 187
  • 2
  • 9
Aman Gupta - ΔMΔN
  • 2,971
  • 2
  • 19
  • 39
  • Would this approach be any better than serializing the object? – Chance Mar 24 '19 at 03:34
  • 1
    @Chance both are different things. This is for inserting the list object in room database using typeconverter simple. And Serialization is a mechanism of converting the state of an object into a byte stream. – Aman Gupta - ΔMΔN Mar 29 '19 at 06:00
  • 2
    The solution using TypeConverters should be used only for small lists with little fields, in case of hundreds of complex objects the performance of TypeConverters is inferior compared to ForeignKey. – yaroslav May 07 '19 at 12:26
  • 17
    This solution is bad for more reasons than just performance. All you are really doing here is flattening objects into JSON strings. You cannot perform queries or do any of the things that make SQL good on flattened strings. – Edward van Raak Jun 20 '19 at 14:12
  • 1
    Is there a way i can save the list data in separate table instead of string in column. because my list has large data nearly 1000 items and want to query those data too. – YLS Jan 06 '20 at 13:28
  • 5
    @EdwardvanRaak there's always exceptions, software development doesn't relies in a single source of truth. So this solution is bad indeed for the reasons you pointed out, but may be good in another scenario you don't need that. – Machado Jul 21 '20 at 17:07
  • This breaks 1NF. – t3ddys Apr 02 '21 at 22:18
  • 2
    It's obvious that this goes against basic database design principles but use cases where the field is for short lists this is not a horrible solution...especially if you don't intend to join on the data. but this certainly simplifies your table schema... – Dexter Legaspi Apr 07 '22 at 17:08
49

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
}

Smirky
  • 187
  • 2
  • 9
Daniel Wilson
  • 18,838
  • 12
  • 85
  • 135
  • 1
    Could you explain a little bit, why this answer is a good solution to the problem? What is Gson doing? It is worth adding such a large library? – nulldroid Oct 22 '19 at 13:09
  • 4
    Gson is used to serialize and deserialize POJOs to Json automatically. You can then pass them around as Strings if type constraints are an issue. I think most apps use either gson or moshi without a second thought. Size / method constraints for something like gson might have been an issue in 2009, but not any more. – Daniel Wilson Oct 23 '19 at 12:48
  • You can use Moshi, which at the time of writing, is really fast or Kotlin Serialisation which is really good if you're planning to store lists and don't want to use Type Tokens. – Andrew Chelix Oct 10 '20 at 09:47
  • 1
    There are multiple versions of how to use TypeConverter. This one is the best option in my case. Thank you ! – clauub Dec 07 '21 at 07:30
15

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).
Davideas
  • 3,226
  • 2
  • 33
  • 51
  • 19
    what you just said seems legit but I didn't understand a single word, could you share some example links – Pemba Tamang Oct 30 '19 at 04:09
  • Could you please expand the example and prepare some content? https://stackoverflow.com/questions/70815892/what-should-we-do-for-nested-objects-in-room/ –  Jan 23 '22 at 12:11
8

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

spaaarky21
  • 6,524
  • 7
  • 52
  • 65
Omkar Amberkar
  • 1,952
  • 4
  • 16
  • 21
  • 1
    do we have to manually create embedded objects while inserting main entity..i read one of the other answers....you opened a bug , is it fixed now ? – anshulkatta Sep 17 '17 at 10:32
  • 1
    One actually can. I can confirm that @Aman's answer worked for me, using the `@TypeConverters` annotation – kip2 May 25 '18 at 18:45
8

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

Leonardo Costa
  • 994
  • 11
  • 26
2

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)
    }
}
Mattia Ferigutti
  • 2,608
  • 1
  • 18
  • 22
0

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 {
    ....
}
shashank J
  • 67
  • 7
-1

I'm coming really late to the party, but I'd really recommend using Kotlin Serialization.

class CountryLangConverter {
    @TypeConverter
    fun toCountryLang(countryLang: String): CountryLang =
        Json.decodeFromString(countryLang)

    @TypeConverter
    fun fromCountryLang(countryLang: CountryLang): String =
        Json.encodeToString(countryLang)
}

And don't forget to check the official documentation about Room and its Type Converters.

Joaquin Iurchuk
  • 5,499
  • 2
  • 48
  • 64